进程
前言
1. 介绍冯诺依曼体系结构
我们常见的计算机或者大厂的后台服务器大部分都遵守冯诺依曼体系。
①. CPU——寄存器、运算器、控制器和时钟
CPU关键组件: 寄存器、运算器、控制器和时钟。
②. 存储器——内存
注意:冯诺依曼体系中的存储器指的是内存。硬件级别的缓存空间
存储的层次结构:
③. 输入输出设备
作用: 与计算机进行交互,将数据和指令从外部传输到计算机,以及将计算机的结果传输到外部。这些设备允许人们通过各种方式与计算机进行通信,包括输入数据、输出结果、控制程序等
注意: 设备之间是相互独立的,需要把这些设备通过“线”连接。“线”又分为:IO总线(进行输入输出)、系统总线(进行数据交互)
但这些设备都是独立的,那如何实现计算机的功能呢?这就需要将这些设备通过"线"连接起来,那我们又将这些线进行分类,进行输入输出的,我们叫做IO总线,进行数据交互的我们叫做系统总线。
④. 程序运行过程
举例:qq
⑤. 小结
2. 操作系统
①. 基本介绍
概念: 操作系统是一款管理软硬件资源的软件
操作系统进行管理的原因: 为了给用户提供一个良好(稳定、高效、安全)的运行环境。手段: 帮助用户管理软硬件资源。
计算机组成部分:
操作系统的加载:
②. 先描述再组织(重要:贯穿Linux内核)
操作系统对软硬件资源的管理:
- 描述:当操作系统想要管理一份软硬件资源时,只需要将这个资源的属性,用结构体描述起来。操作系统通过结构体的信息就可以对这个描述的对象进行管理决策。
- 再组织:很明显软硬件资源不会只有一份,当出现大量的需要管理的资源,就要使用链表或其它的高效数据结构。所以在操作系统中,管理任何对象,都会变成对某种数据结构的增删查改。
进程
1. 概念
概念: 担当分配系统资源的(CPU时间,内存)实体。(哈哈哈哈难以理解,但是后面学到线程还会介绍)。当然有些书上说,正在执行的程序。
当下的理解:进程 = 内核PCB数据结构对象(下面讲) + 代码和数据
2. 了解进程
①进程PCB
在讲操作系统对软硬件资源管理时,强调了对资源如何管理,就是先描述再组织。 操作系统在管理这个进程时不会直接对这个程序进行管理(就比如一个程序体量很大,想要对其进行管理就得先加载到内存,但是有的程序太大,都不能加载到内存),只需要管理这个进程PCB即可。
在进程资源的管理中就是:
- 先描述:进程的PCB,就是进程属性的集合。进程信息被存放在这个PCB结构体中,在Linux操作系统下PCB是:
task_struct
,它会被装载到RAM(内存)里。
Linux内核中task_struct源码:
struct task_struct {
volatile long state; //说明了该进程是否可以执行,还是可中断等信息
unsigned long flags; //Flage 是进程号,在调用fork()时给出
intsigpending; //进程上是否有待处理的信号
mm_segment_taddr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同
//0-0xBFFFFFFF foruser-thead
//0-0xFFFFFFFF forkernel-thread
//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
volatilelong need_resched;
int lock_depth; //锁深度
longnice; //进程的基本时间片
//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR,分时进程:SCHED_OTHER
unsigned long policy;
struct mm_struct *mm; //进程内存管理信息
int processor;
//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1这个值在运行队列被锁时更新
unsigned long cpus_runnable, cpus_allowed;
struct list_head run_list; //指向运行队列的指针
unsigned longsleep_time; //进程的睡眠时间
//用于将系统中所有的进程连成一个双向循环链表,其根是init_task
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_headlocal_pages; //指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式
int exit_code, exit_signal;
intpdeath_signal; //父进程终止是向子进程发送的信号
unsigned longpersonality;
//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
intdid_exec:1;
pid_tpid; //进程标识符,用来代表一个进程
pid_tpgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_tsession; //进程的会话标识
pid_t tgid;
intleader; //表示进程是否为会话主管
struct task_struct*p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; //线程链表
struct task_struct*pidhash_next; //用于将进程链入HASH表
struct task_struct**pidhash_pprev;
wait_queue_head_t wait_chldexit; //供wait4()使用
struct completion*vfork_done; //供vfork()使用
unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
//it_real_value,it_real_incr用于REAL定时器,单位为jiffies,系统根据it_real_value
//设置定时器的第一个终止时间.在定时器到期时,向进程发送SIGALRM信号,同时根据
//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。
//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送
//信号SIGPROF,并根据it_prof_incr重置时间.
//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种
//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据
//it_virt_incr重置初值。
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_listreal_timer; //指向实时定时器的指针
struct tmstimes; //记录进程消耗的时间
unsigned longstart_time; //进程创建的时间
//记录进程在每个CPU上所消耗的用户态时间和核心态时间
longper_cpu_utime[NR_CPUS],per_cpu_stime[NR_CPUS];
//内存缺页和交换信息:
//min_flt, maj_flt累计进程的次缺页数(Copyon Write页和匿名页)和主缺页数(从映射文件或交换
//设备读入的页面数);nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
//cmin_flt, cmaj_flt,cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。
//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsignedlong min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid
//euid,egid为有效uid,gid
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件
//系统的访问权限时使用他们。
//suid,sgid为备份uid,gid
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; //记录进程在多少个用户组中
gid_t groups[NGROUPS]; //记录进程所在的组
//进程的权能,分别是有效位集合,继承位集合,允许位集合
kernel_cap_tcap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
unsigned shortused_math; //是否使用FPU
charcomm[16]; //进程正在运行的可执行文件名
//文件系统信息
int link_count, total_link_count;
//NULL if no tty进程所在的控制终端,如果不需要控制终端,则该指针为空
struct tty_struct*tty;
unsigned int locks;
//进程间通信信息
struct sem_undo*semundo; //进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
//进程的CPU状态,切换时,要保存到停止进程的task_struct中
structthread_struct thread;
//文件系统信息
struct fs_struct *fs;
//打开文件信息
struct files_struct *files;
//信号处理函数
spinlock_t sigmask_lock;
struct signal_struct *sig; //信号处理函数
sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位
struct sigpendingpending; //进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock;
void *journal_info;
};
- 再组织:在系统里进程有很多,所有的进程都以task_struct链表的形式存储在内核。
//用于将系统中所有的进程连成一个双向循环链表,其根是init_task
struct task_struct *next_task, *prev_task;
注:如果再有新的进程增加、删除或修改,直接对该链表进行增删查改
②查看进程信息
查看进程基本信息的手段: ps命令
进程的基本信息:
查看进程pid和ppid
演示:
通过进程pid查看进程相关信息: 操作系统如何找到可执行程序,因为有路径信息的存在。
③fork创建子进程
介绍
认识fork函数 fork之后通常会用if分流,为了区分让不同的执行流执行不同的代码块。
写时拷贝:
注:这里并没有深入理解,如果深入理解,还要牵出线性内存和物理内存,页表等。
使用
简单使用:
写时拷贝:
小结
- 进程 = 内核PCB数据结构对象 + 代码和数据
- 父进程使用fork创建子进程,并进行等待(后面讲)
- 父子进程默认共享代码和数据,如果被修改,会发生写时拷贝
- 进程具有独立性
④僵尸进程
演示僵尸进程:
解决办法:
- 父进程等待子进程(wait)(进程控制博客再说)
- 杀死父进程,子进程资源由OS(init进程或者说1号进程)进行管理释放。
注:kill命令杀不了僵尸进程,因为已经死了
⑤孤儿进程
注:只有父子关系进程,没有爷孙关系的进程,所以释放资源的操作要么是父进程,要么是OS亲自管理。
3. 进程状态
①操作系统学科的进程状态
Ⅰ、运行态
注:
- 具体先调用那个进程,由调度器决定。每个进程都有时间片的概念,就是一个时间段内所有进程的代码都会执行,并发执行。(例如一个运行态的进程10ns)
- 如果一个进程时间片用完,才执行一部分,那该进程的上下文会被打包带走。下次再轮到该进程的时候再恢复上来
- task_struct结构体在运行队列中排队
并发: 一个CPU跑多个程序
并行: 多个CPU跑多个程序
一个CPU只有一个运行队列
寄存器:
Ⅱ、阻塞态
Ⅲ、挂起态
②Linux具体化的进程状态
Ⅰ、R状态(Linux运行态)
注: 前后台进程的区别:(参考)
- 使用权限:后台进程通常没有用户交互界面,只执行一些后台任务,如定时任务、数据同步等;前台进程通常有用户交互界面,用户可以直接与之操作和交互。此外还有运行位置、优先级、系统资源占用。
Ⅱ、S状态(睡眠状态)
注: 该状态,Ctrl+c和kill -9 还能杀死。可唤醒,相应请求
Ⅲ、D状态(深度睡眠状态)
解释: 当进行大量的磁盘写入,你的进程属于睡眠状态,但是如果操作系统资源不够,为了不影响运行态的进程等,直接把该睡眠态的进程杀掉,但是此时文件也没有完全写入磁盘,就会出现错误。所以为了不被打扰,所以出现D状态
Ⅳ、T状态(停止状态)
注: 查看信号(后面信号部分具体介绍)
测试所用代码:
测试:
Ⅴ、 t状态
小结
除此还有x状态(死亡状态)难以演示,还有僵尸状态(上面已经演示)。操作系统的原则和具体操作系统的原则是一致的,但是实现上是不同的。
4. 进程优先级
①概念
②查看进程优先级ps/top命令
③修改进程优先级
top和renice命令调整PRI
- top:
流程图:
注: 想把NI设置成负数,需要root权限
- nice和renice(演示renice)
④大O(1)调度算法
⑤并发,并行等概念
5. 环境变量
查看环境变量的方法:
①举例认识常见的环境变量
1. PATH
在初识Linux基本指令部分就提到。指令就是可执行程序,和我们自己写的代码编译好的程序没有区别。
问题: 为什么Linux的指令执行和我们自己编写好的程序,在执行上有区别?
解释: 因为指令都放在user/bin
的路径下,bash命令行在查找时根据环境变量会到该目录下查找指定的指令。而我们写的程序,没有在该环境变量指定的目录下,所以需要一个完整的目录结构,所以要携带./ (当前路径)
解决办法: 不想添加./
- 把编写的可执行程序移到
user/bin
路径下 - 在环境变量PATH中添加想要执行的程序当前所在的目录(本质就是给PATH增加路径)
方法1就不演示,直接mv过去即可
方法2演示: 增加搜索路径
注:
2. HOME
可以更改,但是更改之后,可能cd ~
该指令就不能使用
3. SHELL
4. env查看其它环境变量
②export创建环境变量
- 将本地变量设置成环境变量,最开始MY_ENV就是被设置成本地变量,继而经过export导入成环境变量
- 直接设置环境变量
③代码中使用环境变量
1. 函数getenv
使用:
所以:可以根据获取同一个环境变量的值不同,做出对应的操作。
2. 命令行参数
在一些教材中,我们可以看到main函数有这样两个参数
int main(int argc, char* argv[])
{}
命令行参数底层存储: 这就是命令行参数表
根据这个表最后一个是NULL,所以我们在获取参数时,也可以这样:
int main(int argc, char* argv[])
{
int i = 0;
for(; argv[i]; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}
3. 环境变量参数
在main函数中还有第三个参数,就是环境变量参数
int main(int argc, char* argv[], char* env[]) //最后一个参数就是环境变量参数
{}
注:我们在使用指令时,大部分都是bash创建子进程执行指令。还有一部分是内建命令,不需要创建子进程。—— 所以我们在查看本地变量是依旧能直接查看,因为本地变量就是在bash中,而echo就属于一个命令。