第三章 进程管理

进程管理

进程是操作系统抽象概念中最基本的一种

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函数完成,流程为:

  1. 调用dup_task_struct()为新进程创建内核栈,task_struct等,此时父子进程描述符内容完全相同。
  2. 检查新进程,进程数目是否超出上限
  3. 子进程着手与父进程区别开,对某些成员清零或设初值。
  4. 新进程状态置为 TASK_UNINTERRUPTIBLE。
  5. 调用copy_flags()更新task_struct的flags成员。
  6. 调用alloc_pid()为新进程分配一个有效的PID
  7. 根据clone()的参数标志,拷贝或共享相应的信息
  8. 做扫尾工作并返回新进程指针

5 线程在Linux中的实现

线程是现代编程技术中常见的抽象概念,该机制提供在同一程序内共享内存地址空间运行一组线程。它们可以共享开发的文件等很多资源。

在Linux内核看来,它没有线程的概念,Linux把所有的线程都当做进程来实现。比如创建四个进程,那么就分配四个task_struct结构,然后指定它们共享的资源即可。

具体创建线程和进程的步骤类似,最后传给clone()函数的参数不同而已。

比如,普通的fork()创建进程,即:

1
clone(SIGCHLD, 0)

创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即:

1
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)

而vfork()为:

1
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0)

传递给clone()的参数标志在uapi/linux/sched.h

在内核运行的线程通常用来在后台执行某些任务。相关概念有个守护进程。内核线程与普通进程的主要区别在于:内核线程没有独立的地址空间(nn指针设置为NULL),它们只在内核空间运行,也可以被调度、被抢占。运行命令ps -ef可以查看内核线程。创建内核线程的接口在linux/kthread.h中,

创建一个新内核线程的方法:

1
2
3
4
5
6
7
struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
void *data,
int node,
const char namefmt[], ...);
#define kthread_create(threadfn, data, namefmt, arg...) \
kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

这与之前提到的Linux内核是个单内核有关。

6. 进程的终止

进程靠do_exit()(定义在kernel/exit.c)来终止执行,主要流程为:

  1. 将task_struct中的标识成员设置为PF_EXITING
  2. 调用del_timer_sync()删除任一内核定时器, 根据返回值确保没有定时器在排队和运行
  3. 调用exit_mm()函数释放进程占用的mm_struct
  4. 调用sem__exit(),使进程离开等待IPC信号的队列
  5. 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源
  6. 把task_struct的exit_code设置为exit()提供的返回值
  7. 调用exit_notify()向父进程发送信号,给子进程重新找养父,并把自己的状态设为EXIT_ZOMBIE,成为僵尸线程
  8. 调用schedule()切换到新进程,处于EXIT_ZOMBIE状态的进程不会再被调度,这是进程执行的最后一段代码,de_exit()永远不返回
  9. 至此,进程相关的所有资源被释放,它只占用内核栈、thread_info和task_struct结构体,等父进程检索信息后这些内存由父进程来释放。

在最终释放进程描述符时调用release_task()

孤儿进程问题

如果子进程的父进程先退出了,那么子进程在退出时,exit_notify()函数会调用forget_original_parent(),find_new_reaper()来寻找新的父进程。

find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。