1.1 进程由来
操作系统进行资源分配和调度的一个独立单位,
1.2 进程描述符
进程是操作系统中调度的实体,需要对进程所必须拥有的资源做抽象描述,这种抽象描述叫进程控制块,Process Control Block PCB,需要描述的信息
- 进程运行状态:就绪 运行 等待阻塞 僵尸
- 程序计数器:记录当前进程运行到哪条指令了
- cpu寄存器:主要保存当前运行的上下文,记录cpu所有必须保存下来的寄存器信息,一遍调度出去之后还能调度回来继续运行
- cpu调度信息:进程优先级,调度队列,调度相关信息
- 内存管理信息:进程使用的内存信息,如进程页表
- 统计信息:进程运行时间等相关统计信息
- 文件相关信息:进程打开的文件。
进程控制块,task_struct 数据结构很大,在进程生命周期内,进程要和内核很多模块进行交互,比如内存管理,进程调度以及文件系统。内核用链表task_list存放所有进程描述符,task_struct包含内容:
- 进程属性相关信息
- 进程间的关系
- 进程调度相关信息
- 内存管理相关信息
- 文件管理相关信息
- 信号的相关信息
- 资源限制的相关信息
1.3 进程生命周期
- TASK_RUNNING 就绪态
- TASK_INTERRUPTIBLE 可中断睡眠态
- TASK_UNINTERRUPTIBLE 不可中断态
- __TASK_STOPOPED 终止态
- EXIT_ZOMBIE 僵尸态
1.4 进程标识
Process Identifier pid bitmap机制,保证每个进程创建时都能分配到唯一的号码
线程组
1.5 进程间的家族关系
1.6 获取当前进程
2 进程的创建与终止
2.1 写时复制技术
创建新进程时复制父进程所拥有的所有资源, 写时复制 copy on write COW,父进程创建子进程时不需要复制进程地址空间的内容给子进程,只需要复制父进程的进程地址空间的页表给子进程,这样,子进程就可以共享相同的物理内存,当父子进程有一方需要修改某个物理页面的内容时,触发写保护的缺页异常,然后才把共享页面的内容复制出来,从而让父子进程拥有各自的副本。也就是说,地址空间以支付方式共享,当需要写入时才发生复制。
写时复制可以推迟甚至避免复制数据。fork() 创建一个新进程的开销变得很小,之前需要复制父进程整个地址空间, 现在只需要复制父进程的页表,开销很小。
2.2 fork()函数
子进程和父进程拥有各自独立的进程地址空间,但是共享物理内存资源,包括:进程上下文,进程栈,内存信息,打开的文件描述符,进程优先级,根目录,资源限制,控制中断等,创建期间,父子进程共享物理内存空间,当他们开始运行各自程序时,他们的进程地址空间开始分道扬镳,得益于写时复制技术的优势。
父子进程区别:
- pid不同
- 子进程不会继承父进程内存方面的锁
- 子进程不会继承父进程的一些定时器
- 不会继承父进程的信号量
2.3 vfork()函数
vfork vs fork
- fork()子进程拷贝父进程的 数据段 代码段, vfork() 子进程和父进程共享数据段
- fork() 父子进程执行次序不确定 ,vfork() 保证子进程先运行,调用exec或exit之前与父进程数据时共享的,调用exec或exit之后父进程才可能被调度执行。
- vfork保证子进程先运行,调用exec或exit之后父进程才可能被调度执行,如果在调用这两个函数之前子进程依赖父进程的进一步动作,会导致死锁。
2.4 clone函数
进程的四要素:
(1)有一段程序供其执行(不一定是一个进程所专有的),就像一场戏必须有自己的剧本。
(2)有自己的专用系统堆栈空间(私有财产)
(3)有进程控制块(task_struct)(“有身份证,PID”)
(4)有独立的存储空间。
缺少第四条的称为线程,如果完全没有用户空间称为内核线程,共享用户空间的称为用户线程
clone是Linux为创建线程设计的(虽然也可以用clone创建进程)。所以可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。
2.5 内核线程
内核经常需要在后台执行一些操作,这种任务就可以通过内核线程(kernle thread)完成,内核线程是独立运行在内核空间的标准进程。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm指针被设置为NULL;它只在内核空间运行,从来不切换到用户空间去;并且和普通进程一样,可以被调度,也可以被抢占。
2.6 do_fork()函数
操作系统通过系统调用fork(),vfork()和clone()函数来完成进程的创建,最终都调用了内核函数do_fork(),
2.7 终止进程
两种方式:
- 主动终止,显式执行exit系统调用,或者主函数返回
- main函数返回自动添加exit系统调用
- 主动执行exit系统调用
- 被动终止,接收到终止信号,或异常时终止
- 进程收到一个自己不能处理的信号
- 进程在内核态执行时产生了一个异常
- 进程收到SIGKILL等终止信号
- 终止时候释放占有的资源,并把消息告诉父进程
- 先于父进程终止,子进程变成僵尸进程,直到父进程调用wait才算最终消亡,
- 父进程之后终止,init进程称为子进程新的父进程
2.8 僵尸进程和托孤进程
僵尸进程产生的过程是:父进程调用 fork() 创建子进程后,子进程运行直至其终止,它立即从内存中移除,但进程描述符仍然保留在内存中(进程描述符占有极少的内存空间)。子进程的状态变成 EXIT_ZOMBIE,并且向父进程发送 SIGCHLD 信号,父进程此时应该调用 wait() 系 统调用来获取子进程的退出状态以及其它的信息。在 wait() 调用之后,僵尸进程就完全从内存中移除。因此一个僵尸进程存在于其终止到父进程调用 wait() 等函数这个时间的间隙,一般很快就消失,但如果编程不合理(父进程是个死循环),父进程从不调用 wait() 等系统调用来收集僵尸进程,那么这些进程会一直存在内存中。清除僵尸进程的两种方式:改写父进程,杀死父进程,子进程过继给1号进程init。
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了 init 进程(或者 systemd 进程)身上
2.9 进程0和进程1
进程0:别名 idle进程,swapper进程。 内核线程,系统创建的第一个进程,并且还是唯一一个没有通过kernel_thread以及它所创建的子类进程所创建的进程,在进程调度中起着重要作用。
进程1:init进程,初始化系统配置
3 进程调度
进程调度器:一个进程拥有处理器资源,其他进程只能在就绪队列runqueue队列中等待,等到处理器空闲才有机会获取处理器资源并运行。操作系统在众多的就绪进程中选择要给最合适的进程。一个进程运行过程有可能需要等待某些资源,比如磁盘操作完成,等待键盘输入,等待物理页面的分配。如果处理器和进程一起等待,很明显浪费处理器资源,所以一个进程在睡眠等待时,调度器可以调度其他进程来运行,提高处理器的利用率。
3.1 进程分类
- cpu消耗型 CPU-Bound
- 大部分时间用来执行代码上,一直占用cpu,while循环。大量数学计算 matlab
- I/O消耗型 I/O-Bound
- 大部分时间在提交I/O请求或者等待I/O请求。只需要很少的处理器计算资源即可,如需要键盘输入的进程,等待网络I/O的过程。
3.2 进程优先级和权重
经典的调度算法时基于优先级调度,nice值 0~99 实时进程,100~139 普通进程,task_struct 有四个成员描述进程优先级
- static_prio 静态优先级,进程启动时分配,内核不存储nice值,而是static_prio,静态,不随时间而改变
- normal_prio 基于静态优先级策略计算出来的优先级 ,创建时继承自父进程,普通进程等同static_prio,对于实时进程,会根据rt_priority,重新计算normal_prio
- prio 保存进程的动态优先级
- rt_priority
除了优先级表示进程的轻重缓急,调度器还有权重的概念来表示进程的优先级
3.3 调度策略
schedule policy,内核把相同的调度策略抽象成调度类,schedule class,不同的进程采用不同的调度策略,内核中默认实现了5个调度类,
调度类 | 调度策略 | 适用范围 | 说明 |
---|---|---|---|
stop | 无 | 最高优先级,比deadline进程优先级高 | - 可以抢占任何进程 内核线程,优先级最高 负载均衡机制的进程迁移,cpu热插拔,RCU等 |
deadline | SCHED_DEADLINE | 最高优先级实时进程,优先级-1 | 用于调度有严格时间要求的实时进程,比如视频编解码 |
realtime | SCHED_FIFO SCHED_RR | 普通实时进程,0~99 | IRQ线程化 |
CFS | SCHED_NORMAL SCHED_BATCH SCHED_IDLE | 普通进程 100~139 | CFS调度 |
idle | 无 | 最低优先级 | 就绪队列没有其他进程时进入idle调度类,让cpu进入低功耗模式 |
3.4 时间片
time slice,进程调度进来与被调度出去之间,所能持续运行的时间长度。很难确定多长的时间合适,太长导致交互型进程得不到及时响应,过短增大进程切换带来的处理器消耗。I/O消耗性型进程不需要很长的时间片,cpu消耗型希望时间片越长越好
3.5 经典调度算法
3.6 Linux O(n)调度算法
3.7 Linux O(1)调度算法
3.8 Linux CFS算法
3.9 进程切换
3.10 与进程相关的数据结构
4 多核调度
4.1 调度域和调度组
4.2 负载的计算
4.3 负载均衡算法
4.4 Per-CPU变量
- 进程栈
- 虚拟地址4G,最高1G内核空间 内核空间,所有进程共享,较低3G用户空间,每个进程通过系统调用进入内核态。
- 程序段:可执行文件代码的内存映射
- 数据段:可执行文件已初始化全局变量的内存映射
- BSS段:未初始化全局变量或者静态变量
- 堆区:存储动态内存分配,匿名的内存映射
- 栈区: 进程用户空间栈,由编译器自动分配释放,存放函数参数值,局部变量的值 此即为进程栈
- 映射段:任何内存映射文件
- 进程栈初始化大小由编译器和链接器计算出来
- 虚拟地址4G,最高1G内核空间 内核空间,所有进程共享,较低3G用户空间,每个进程通过系统调用进入内核态。
- 线程栈
- 从内核角度看,并没有线程的概念,Linux把所有线程都当作进程来实现。它将线程和进程不加区分的统一到task_struct,线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎时进程和Linux线程唯一的区别。线程创建的时候加上CLONE_VM标记,这样线程的内存描述符将直接指向父进程的内存描述符。
- 内核栈
- 在每个进程的生命周期中,在执行系统调用陷入内核态之后, 内核代码所使用的栈并不是原先进程用户空间的栈,而是一个单独内核空间的栈,这个称作进程内核栈。进程内核栈在进程创建的时候通过slab分配器从thread_info_cache缓存池中分配出来,大小为THREAD_SIZE,一般来说就是一个页的大小4K
- 中断栈