简介
什么是进程可以通过几个方面来阐述,首先是进程在操作系统中的作用,进程在代码层有哪些结构,进程在操作系统中运行时的生命周期
线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。pthread_create 不是一个系统调用,是 Glibc 库的一个函数
线程之间共享进程的内存空间(包括代码段、数据段、堆等)以及以下进程级的资源(如打开文件和信号)。
进程的作用
进程是计算机上可执行程序的运行时表现,包含了对内存、硬盘、cpu等硬件的操作指令,从而实现各种计算以及存储的功能。可以说,操作系统就是由一个又一个的进程组成的
进程的数据结构
在内核代码上,进程的所有相关操作都能在其结构体的字段上体现
任务标识
线程与进程并没有本质上的区别,特别是在代码的结构体表现上,都是使用的同一个结构体,只是线程结构体的pid(线程自己的id)和tgid(进程中主线程的id)是不一样的,也就是通过tgid我们就能判断一个task_struct是进程还是线程了
信号
记录了哪些信号被阻塞暂不处理(blocked),哪些信号尚等待处理(pending),哪些信号正在通过信号处理函数进行处理(sighand)
并且其中signal字段还有个shared_pending用来表示整个线程组共享的信号
任务状态

TASK_KILLABLE,可以终止的新睡眠状态。进程处于这种状态中,它的运行原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号。
运行信息
用户态消耗的CPU、内核态消耗的CPU、自愿上下文切换次数、非自愿上下文切换次数,不包含睡眠时间的进程启动时间,包含睡眠时间的进程启动时间
进程亲缘关系
任何一个进程都有一个父进程,整个进程关系是一个进程树的。
结构体信息中,包含了对父进程、子进程以及兄弟进程的引用
进程权限
包含了谁可以执行、控制进程,以及文件的保存、以及操作消息队列、共享内存、信号量等权限
内存管理
每个进程自己独立的虚拟内存空间
堆和栈是虚拟概念
程序在运行期间可以主动从堆区申请内存空间,这些内存由内存分配器分配并由垃圾收集器负责回收。栈区的内存由编译器自动进行分配和释放,栈区中存储着函数的参数以及局部变量,它们会随着函数的创建而创建,函数的返回而销毁。
栈由一个个栈帧组成,栈帧保存了一个函数调用所需要维护的信息,包括返回地址和参数,临时变量,保存的上下文
- 用户态函数栈空间

- 内核态函数栈空间
当系统调用从用户态到内核态的时候,首先要做的第一件事情,就是将用户态运行过程中的 CPU 上下文保存起来,其实主要就是保存在pt_regs结构体寄存器变量里。
这样当从内核系统调用返回的时候,才能让进程在刚才的地方接着运行下去。
文件与文件系统
文件系统的信息、打开的文件的信息
进程调度
定义了进程的优先级(用于指导如何进行cpu资源分配的指标),调度器类,调度实体,调度策略
进程的生命周期
进程的创建
fork 的第一件大事:复制结构,然后创建内核栈,重新设置进程运行的统计量,设置调度相关的变量。初始化与文件和文件系统相关的变量,包括复制一个进程打开的文件信息,将所有的文件描述符数组 fdtable 拷贝一份。还有复制一个进程的目录信息。初始化与信号相关的变量。
然后就是,将进程状态设置为TASK_RUNNING。等待分配到cpu进行执行,如果新创建的进程应该抢占父进程,fork 是一个系统调用,从系统调用返回的时候,是抢占的一个好时机,如果父进程判断自己已经被设置为 TIF_NEED_RESCHED,就让子进程先跑,抢占自己。
上下文切换
对于进程的上下文切换主要干两件事情,一是切换进程空间,也即虚拟内存;二是切换寄存器和 CPU 上下文。
- 1、切换页表全局目录
- 2、切换内核态堆栈
- 3、切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文)
- ip(instruction pointer):指向当前执行指令的下一条指令
- bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
- sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
- cr3:页目录基址寄存器,保存页目录表的物理地址
- 4、刷新TLB
- 5、系统调度器的代码执行
所谓的进程切换,就是将某个进程的 thread_struct 里面的寄存器的值,写入到 CPU 的 TR 指向的 tss_struct,对于 CPU 来讲,这就算是完成了切换。
- 查看进程的运行时间:
ps -o etime= -p [进程id]
- 查看进程的上下文切换次数:
grep ctxt /proc/[pid]/status
, 其中volutary_ctxt_switches表示主动调度的次数,nonvolutary_ctxt_switches表示抢占式调度的次数
对于线程上下文切换来说,由于线程之间大部分数据是共用的,所以在切换时像进程空间、TLB、页表全局目录时不需要切换的,只需要切换部分硬件数据,例如当前函数的栈顶sp、栈底bp地址、一些命令地址pc
进程调度
优先级
无论是只有一个cpu的时代,还是多核cpu时代,都是通过控制进程占用cpu时间的长短来实现的。就是说在同一个调度周期中,优先级高的进程占用的时间长些,而优先级低的进程占用的短些。
Linux系统中运行的进程可以分成两类:实时进程和非实时进程,实时操作系统需要保证相关的实时进程在较短的时间内响应,不会有较长的延时,并且要求最小的中断延时和进程切换延时。对于这样的需求,一般的进程调度算法,无论是O1还是CFS都是无法满足的,所以内核在设计的时候,将实时进程单独映射了100个优先级,这些优先级都要高与正常进程的优先级(nice值),而实时进程的调度算法也不同,它们采用更简单的调度算法来减少调度开销。
nice值
nice值, 静态优先级, 反应一个进程“优先级”状态的值,其取值范围是-20至19,一共40个级别,映射到实际的优先级范围是100-139。这个值越小,表示进程”优先级”越高,而值越大“优先级”越低。
越nice的人抢占资源的能力就越差,而越不nice的人抢占能力就越强。
- nice命令来对一个将要执行的命令进行nice值设置,
nice -n 10 bash
- 使用renice命令可以对一个正在运行的进程进行nice值的调整
priority
priority, 动态优先级
调度策略
#define SCHED_NORMAL 0
#define SCHED_FIFO 1 # 以先进先出的队列方式进行调度,在优先级一样的情况下,谁先执行的就先调度谁,除非它退出或者主动释放CPU。
#define SCHED_RR 2 # 以时间片轮转的方式对相同优先级的多个进程进行处理。时间片长度为100ms。
#define SCHED_BATCH 3
#define SCHED_IDLE 5 # 如果一个进程被标记成了SCHED_IDLE策略,调度器将认为这个优先级是很低很低的,比nice值为19的优先级还要低。系统将只在CPU空闲的时候才会对这样的进程进行调度执行。
#define SCHED_DEADLINE 6
- 实时进程的调度策略:SCHED_FIFO、SCHED_RR、SCHED_DEADLINE。
- 普通进程的调度策略:SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE。
系统的整体优先级策略是:如果系统中存在需要执行的实时进程,则优先执行实时进程。直到实时进程退出或者主动让出CPU时,才会调度执行非实时进程。实时进程可以指定的优先级范围为1-99,将一个要执行的程序以实时方式执行的方法为:
chrt 10 bash
chrt -p $$
O1调度
老版本linux的调度策略,O1调度算法是在Linux 2.6开始引入的,到Linux 2.6.23之后内核将调度算法替换成了CFS。
O1调度器仍然是根据经典的时间片分配的思路来进行整体设计的。
要实现优先级则是将时间片分配成大小不等的若干种,优先级高的进程使用大的时间片,优先级小的进程使用小的时间片。
为了提高IO消耗型进程的响应速度,例如文本编辑类的程序,系统将区分这两类进程,并动态调整CPU消耗的进程将其优先级降低,而IO消耗型的将其优先级变高,以降低CPU消耗进程的时间片的实际长度。
随着其不断执行,内核会观察进程的CPU消耗状态,并动态调整priority值,可调整的范围是+-5。就是说,最高其优先级可以呗自动调整到115,最低到125。这也是为什么nice值叫做静态优先级而priority值叫做动态优先级的原因。不过这个动态调整的功能在调度器换成CFS之后就不需要了,因为CFS换了另外一种CPU时间分配方式
CFS完全公平调度
O1已经是上一代调度器了,由于其对多核、多CPU系统的支持性能并不好,并且内核功能上要加入cgroup等因素,Linux在2.6.23之后开始启用CFS作为对一般优先级(SCHED_OTHER)进程调度方法。
如果当前有n个进程需要调度执行,那么调度器应该再一个比较小的时间范围内,把这n个进程全都调度执行一遍,并且它们平分cpu时间,这样就可以做到所有进程的公平调度。那么这个比较小的时间就是任意一个R状态进程被调度的最大延时时间,即:任意一个R状态进程,都一定会在这个时间范围内被调度相应。这个时间也可以叫做调度周期,其英文名字叫做:sched_latency_ns。进程越多,每个进程在周期内被执行的时间就会被平分的越小。
调度器只需要对所有进程维护一个累积占用CPU时间数,就可以衡量出每个进程目前占用的CPU时间总量是不是过大或者过小,这个数字记录在每个进程的vruntime中。所有待执行进程都以vruntime为key放到一个由红黑树组成的队列中,每次被调度执行的进程,都是这个红黑树的最左子树上的那个进程,即vruntime时间最少的进程,这样就保证了所有进程的相对公平。
在CFS调度中,进程的优先级是以时间消耗(vruntime增长)的快慢来决定的。优先级越高的进程,它的vruntime增长的越慢,也就更容易调度。
对于新进程的创建,如果新产生的进程直接将自己的vruntime值设置为0的话,那么它将在执行开始的时间内抢占很多的CPU时间,直到自己的vruntime追赶上其他进程后才可能调度其他进程,这种情况显然是不公平的。所以CFS对每个CPU的执行队列都维护一个min_vruntime值,这个值纪录了这个CPU执行队列中vruntime的最小值,当队列中出现一个新建的进程时,它的初始化vruntime将不会被设置为0,而是根据min_vruntime的值为基础来设置。这样就保证了新建进程的vruntime与老进程的差距在一定范围内,不会因为vruntime设置为0而在进程开始的时候占用过多的CPU。
对于IO消耗型的应用,它的vruntime是不怎么变的,相当于每次调用会优先调用这些进程,不会存在O1调度中的需要动态调整优先级的问题
进程间通信
管道模型
匿名管道和命名管道分别叫做PIPE和FIFO
文件类型p表示一个管道文件。有了这个管道文件,系统中就有了对一个管道的全局名称,于是任何两个不相关的进程都可以通过这个管道文件进行通信了
管道的写操作是阻塞的,这是内核对管道文件定义的默认行为。此时如果有进程读这个管道,那么这个写操作的阻塞才会解除
mkfifo pipe
ls -l pipe
# prw-r--r-- 1 wwqdrh wwqdrh 0 Jul 14 10:44 pipe
echo xxxxxxxxxxxxxx > pipe
cat pipe
文件模型
考虑到系统对文件本身存在缓存机制,使用文件进行IPC的效率在某些多读少写的情况下并不低下
不过使用文件进行通信,会有竞态问题存在,需要引入文件锁进行处理。
从底层的实现来说,Linux的文件锁主要有两种:flock和lockf。需要额外对lockf说明的是,它只是fcntl系统调用的一个封装。从使用角度讲,lockf或fcntl实现了更细粒度文件锁,即:记录锁。我们可以使用lockf或fcntl对文件的部分字节上锁,而flock只能对整个文件加锁。
共享内存模型
如果使用共享内存,除了需要自己手工构造一个可能不够高效的队列外,我们还要自己处理竞争条件和临界区代码。很麻烦
- mmap内存共享映射。
- XSI共享内存。
- POSIX共享内存。
消息队列模型
- xsi消息队列
- posix消息队列
信号量模型
pv操作