进程管理
进程是操作系统抽象概念中最基本的一种
1. 进程和线程
进程是程序运行的实例,是操作系统分配资源的基本单元,因此,进程除了可执行代码(代码段,text section)外,还包括其他资源,如打开的文件、信号、数据段、堆栈等,一个进程包含一个多个执行线程(thread of execution),对进程的管理由内核完成。
执行线程,简称线程,线程属于某个进程中的活动对象,线程共享进程的地址空间和数据,但每个线程有自己的程序计数器、线程栈和寄存器,内核调度的对象是线程,而不是进程。
Linux内核不区分线程和进程,而是称task,对应task_struct结构体。
进程提供了2种虚拟机制:虚拟处理器和虚拟内存,每个进程有独立的虚拟处理器和虚拟内存,每个线程有独立的虚拟处理器,线程间可能共享虚拟内存。
2. 进程描述符和任务结构
Linux定义结构体task_struct来描进程,该结构体在linux/sched.h中,称为进程描述符或PCB(进程控制模块),包含了进程的所有信息,内核将进程存放在一个叫做任务队列(task list)的双向循环链表中。
thread_info创建在内核栈尾端用于计算偏移间接查找task_struct结构,因为内核大部分处理进程的代码都是直接通过task_struct进行,因此查找当前正在运行进程的task_struct速度非常重要。/ 最新内核已改变,需要进一步学习 /
3.进程状态
进程有五种状态:
- TASK_RUNNING: 运行
- TASK_INTERRUPTIBLE:可中断
- TASK_UNINTERRUPTIBLE:不可中断
- __TASK_TRACED:可跟踪
- __TASK_STOPPED: 停止
用户进程只有通过系统调用或中断处理程序才能陷入内核执行。
4. 进程创建
有些系统提供产生(spawn)进程的机制,而Linux中创建进程由两个系统调用实现:fork()和exec()。
fork: 通过拷贝当前进程创建一个子进程
此时父子进程之间的区别仅仅在于PID、PPID、某些资源和统计量
exec: 读取可执行文件,将其载入到地址空间中运行
Linux通过clone()系统调用实现fork(),而clone()调用do_dork(),最终由copy_process函数完成,流程为:
- 调用dup_task_struct()为新进程创建内核栈,task_struct等,此时父子进程描述符内容完全相同。
- 检查新进程,进程数目是否超出上限
- 子进程着手与父进程区别开,对某些成员清零或设初值。
- 新进程状态置为 TASK_UNINTERRUPTIBLE。
- 调用copy_flags()更新task_struct的flags成员。
- 调用alloc_pid()为新进程分配一个有效的PID
- 根据clone()的参数标志,拷贝或共享相应的信息
- 做扫尾工作并返回新进程指针
5 线程在Linux中的实现
线程是现代编程技术中常见的抽象概念,该机制提供在同一程序内共享内存地址空间运行一组线程。它们可以共享开发的文件等很多资源。
在Linux内核看来,它没有线程的概念,Linux把所有的线程都当做进程来实现。比如创建四个进程,那么就分配四个task_struct结构,然后指定它们共享的资源即可。
具体创建线程和进程的步骤类似,最后传给clone()函数的参数不同而已。
比如,普通的fork()创建进程,即:
创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即:
而vfork()为:
传递给clone()的参数标志在uapi/linux/sched.h
在内核运行的线程通常用来在后台执行某些任务。相关概念有个守护进程。内核线程与普通进程的主要区别在于:内核线程没有独立的地址空间(nn指针设置为NULL),它们只在内核空间运行,也可以被调度、被抢占。运行命令ps -ef可以查看内核线程。创建内核线程的接口在linux/kthread.h中,
创建一个新内核线程的方法:
这与之前提到的Linux内核是个单内核有关。
6. 进程的终止
进程靠do_exit()(定义在kernel/exit.c)来终止执行,主要流程为:
- 将task_struct中的标识成员设置为PF_EXITING
- 调用del_timer_sync()删除任一内核定时器, 根据返回值确保没有定时器在排队和运行
- 调用exit_mm()函数释放进程占用的mm_struct
- 调用sem__exit(),使进程离开等待IPC信号的队列
- 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
- 把task_struct的exit_code设置为exit()提供的返回值
- 调用exit_notify()向父进程发送信号,给子进程重新找养父,并把自己的状态设为EXIT_ZOMBIE,成为僵尸线程
- 调用schedule()切换到新进程,处于EXIT_ZOMBIE状态的进程不会再被调度,这是进程执行的最后一段代码,de_exit()永远不返回
- 至此,进程相关的所有资源被释放,它只占用内核栈、thread_info和task_struct结构体,等父进程检索信息后这些内存由父进程来释放。
在最终释放进程描述符时调用release_task()
孤儿进程问题
如果子进程的父进程先退出了,那么子进程在退出时,exit_notify()函数会调用forget_original_parent(),find_new_reaper()来寻找新的父进程。
find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。