c">目录
c" style="margin-left:80px;">进程描述符
c" style="margin-left:80px;">标识一个进程
c" style="margin-left:80px;">进程组织
c" style="margin-left:80px;">进程之间的关系
c" style="margin-left:80px;">如何组织进程
c" style="margin-left:80px;">等待队列
c" style="margin-left:40px;">进程资源限制
c" style="margin-left:40px;">进程切换
c" style="margin-left:80px;">硬件上下文
ch_to%E5%AE%8F-toc" style="margin-left:80px;">switch_to宏
c" style="margin-left:40px;">创建进程
c" style="margin-left:80px;">do_fork
c" style="margin-left:80px;">内核进程
c" style="margin-left:40px;">撤销有一个进程
c" style="margin-left:80px;">do_group_exit
c" style="margin-left:80px;">do_exit函数
ce-toc" style="margin-left:0px;">Reference
下面开始讨论一个非常重要的抽象:这在我们前边已经提到过了的!也就是进程!
进程在使用中常有几个不同的含义c;比如说在一般的操作系统教科书中给出了通用定义是:
进程是程序执行的一个实例
进程就像生命一般他们被产生c;有或多或少的有效生命周期c;c;可以产生一个或者多个子进程c;但是它们最终都会死亡!从内核观点上看进程的目的就是分配系统资源的一个实体
当一个进程创建时c;它几乎与父进程相同。它接收父进程地址空间的一个逻辑拷贝c;并开始执行父进程相同的代码!
Linux使用轻量级进程c;对多线程应用程序提供了更好的支持。两个轻量级进程基本上可以共享一些资源诸如打开的文件c;地址空间等。只要其中一个修改了共享资源c;另一个就立即查看各种修改
为了管理进程c;内核必须对每个进程所做的事情有清楚的描述!
首先我们要看的是进程的状态:顾名思义当我们查看源码时c;进程描述符中的state字段就描述了进程当前的状态。它由一组标志组成。有以下几种可能的状态:
可运行状态(TASK_RUNNING):其进程要么在CPU上执行要么准备执行与
可中断的等待状态(TASK_INTERRUPTIBLE):进程被挂起直到某个条件为真c;产生一个硬件中断c;释放进程c;正等待的系统资源或传递一个信号都是可以唤醒进程的条件
不可中断的等待状态(TASK_UNINTERRUPTIBLE):与可中断的等待状态类似c;但是有一个例外:把信号传递到睡眠进程不能改变它的状态。这种状态很少用到c;但在一些特定的情况下c;也就是要求进程必须等待直到一个不能被中断的事件发生时c;这种状态很有用!例如当进程打开一个设备文件c;设备驱动程序开始探测相应的硬件设备时会用到这个状态c;探测完成以前设备驱动程序是不可以被中断的!否则硬件设备会处于一种不可预知的状态!
暂停状态(TASK_STOPPED):进程的执行被暂停c;当进程接收到这四个信号:SIGSTOPc;SIGTSTPc;SIGTTINc;SIGTTOUc;进入暂停状态
跟踪状态(TASK_TRACED):进程的执行已经被调试程序暂停c;当一个进程被另一个进程监控时c;任何信号都可以把这个进程置于这个状态!
还有两个进程状态c;是既可以存放在进程描述符当中c;c;也可以存放在退出状态字段当中!
僵死状态:进程的执行被终止c;但是父进程并没有发布wait4或wait pid系统调用来返回有关死亡进程的信息
将死撤销状态:最终状态由于父进程发出了上面提到的两个系统调用c;因而进程由系统进行删除。为了防止其他执行进程在同一进程上也执行wait类系统调用而把进程的状态由僵死状态设为僵死撤销状态!
一般来说能被独立调度的每个进程上下文都必须有它自己的进程描述符!也就是pid。pid存放在进程描述符的pid字段中c;它被顺序编号。内核使用一个pid_map_array位图来表示当前已经被分配的pid号和闲置的pid号。
进程描述符处理进程是动态实体c;因此内核必须能够同时处理很多进程!并把进程描述符存放在动态内存中c;而不是永久分配给内核的内存区!
标识当前进程是使用一个叫做thread_info的结构。进程最常用地址不是thread_info地址而是进程描述符的地址c;为了获得当前CPU上运行形成的描述符指针内核需要调用current宏。该宏本质上等价于<code>current thread info()->taskcode>
Linux使用双向链表来管理进程!被称为进程链表。
task_running状态的进程列表c;当内核寻找一个新进程在CPU上运行时c;必须只考虑可以运行的。进程在Linux 2.6实现的运行队列与之前的实现有所不同c;其目的是让调度程序能够在固定时间内选出最佳的可知运行进程!与队列中的可运行的进程数无关
程序创建的进程具有父子关系。如果一个进程创建多个子进程c;则多个子进程之间又有兄弟关系。进程描述符中表示进程亲属关系的字段描述如下:
描述符 | 说明 |
---|---|
real parent | 指向创建了P的进程的描述符,如果P的进程不再存在就指向进程init进程描述符 |
parent | P的当前父进程c;它的值通常与real parent一致。但偶尔有所不同c;比如说另一个进程发出监控P的Ptrace调用时。 |
Children | 链表的头部链表中的所有元素都是P创建的子进程 |
sibling | 指向兄弟进程链表中的下一个元素c;或前一个元素的指针。这些兄弟进程的父进程都是P |
运行队列链表把处于task running状态的所有进程组织在c;一起不同的状态要求不同的处理!Linux选择下面的方式之一:
没有未处理<code>task_stoppedcode>和<code>exit_zombiecode>或者是<code>exit_deadcode>状态的进程建立专门的链表。由于对处于暂停僵死c;死亡状态进程的访问比较简单c;或者通过pid或者通过特定父进程的子进程链表。所以不必对这三种状态进行分组
根据不同的特殊事件把处于<code>task_interrupttablecode>或<code>task_uninterruptablecode>状态的进程细分为许多类c;每个类都对应一个特殊事件。
在这种情况下进程状态提供的信息满足不了快速检索进程的需要c;所以需要另外引入进程列表!
等待队列在内核中有很多用途!特别是在处理中断处理进程同步及定时(进程必须经常等待某些事情的发生——等待队列是由双向链表所实现的!关于具体操作可以参看相关的博客。
每一个进程都有资源限制:
struct rlimit {rlim_t rlim_cur; /* Soft limit */rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */};
这是常见的一些限制
RLIMIT_AS
RLIMIT_CORE
RLIMIT_CPU
进程最多使用的 CPU 时间(包括系统模式和用户模式)。
当超过此软限制时c;向该进程发送SIGXCPU信号(SIGXCPU 信号的默认动作是终止一个进程并输出一个核心 dump。此外c;也可以捕获这个信号并将控制返回给主程序。)。
不同的 UNIX 实现对进程处理完 SIGXCPU 信号之后继续消耗 CPU 时间这种情况的处理方式不同。大多数会每隔固定时间间隔向进程发送一个 SIGXCPU 信号。
在达到软限制值之后c;Linux 内核会在进程每消耗一秒钟的 CPU 时间后向其发送一个 SIGXCPU 信号。当进程持续执行直至达到硬 CPU 限制时c;内核会向其发送一个 SIGKILL 信号c;该信号总是会终止进程。
RLIMIT_DATA
RLIMIT_FSIZE
RLIMIT_MEMLOCK
RLIMIT_MSGQUEUE
能够为调用进程的真实用户 ID 的 POSIX 消息队列分配的最大字节数。
RLIMIT_MSGQUEUE 限制只会影响调用进程。这个用户下的其他进程不会受到影响c;因为它们也会设置这个限制或继承这个限制。
RLIMIT_NICE
RLIMIT_NOFILE
一个进程能够分配的最大文件描述符数量加 1
试图(如 open()、pipe()、socket()、accept()、shm_open()、dup()、dup2()、fcntl(F_DUPFD)和 epoll_create())分配的文件描述符数量超出这个限制时会失败。
更改此限制将影响到sysconf函数在参数_SC_OPEN_MAX中返回的值
在 Linux 上可以通过使用 readdir()扫描/proc/PID/fd 目录下的内容来检查一个进程当前打开的文件描述符c;这个目录包含了进程当前打开的每个文件描述符的符号链接。
从 2.6.25 的版本开始c;这个限制由 Linux 特有的/proc/sys/fs/nr_open 文件定义。这个文件中的默认值是 1048576c;超级用户可以修改这个值。试图将软或硬 RLIMIT_NOFILE 限制设置为一个大于最大值的值会产生 EPERM 错误。
还存在一个系统级别的限制c;它规定了系统中所有进程能够打开的文件数量c;通过 Linux 特有的/proc/sys/fs/file-max 文件能够获取和修改这个限制。
只有特权(CAP_SYS_ADMIN)进程才能够超出 file-max 的限制。
在非特权进程中c;当系统调用碰到 file-max 限制时会返回 ENFILE 错误
RLIMIT_NPROC
规定了调用进程的真实用户 ID 下最多能够创建的进程数量。
试图(fork()、vfork()和 clone())超出这个限制会得到 EAGAIN 错误
RLIMIT_NPROC 限制只影响调用进程。这个用户下的其他进程不会受到影响c;除非它们也设置或继承了这个限制。这个限制不适用于特权(CAP_SYS_ADMIN 和 CAP_SYS_RESOURCE)进程。
Linux 还提供了系统层面的限制来规定所有用户能够创建的进程数量。在 Linux 2.4以及之后的版本中c;可以使用 Linux 特有的/proc/sys/kernel/threads-max 文件来获取和修改这个限制
准确地说c;RLIMIT_NPROC 资源限制和 threads-max 文件实际上限制的是所能创建的线程数量c;而不是进程的数量
更改此限制将影响到sysconf函数在参数_SC_CHILD_MAX中返回的值
不存在一种统一的方法能够在不同系统中找出某个特定用户 ID 已经创建的进程数。
RLIMIT_RSS
RLIMIT_RTPRIO
RLIMIT_RTTIME
RLIMIT_SBSIZE
在任一给定时刻c;一个用户可以占用的套接字缓冲区的最大长度(字节)
RLIMIT_SIGPENDING - 一个进程可排队的信号最大数量制(Linux 特有的c;自 Linux 2.6.8 起) - 试图(sigqueue())超出这个限制会得到 EAGAIN错误。 - RLIMIT_SIGPENDING 只影响调用进程。这个用户下的其他进程不会受到影响c;除非它们也设置或继承了这个限制
RLIMIT_STACK
RLIMIT_VMEN
这时RLIMIT_AS的同义词
为了控制进程的执行c;内核必须有能力挂起正在CPU上运行的进程!以恢复以前挂起的某个进程的执行。这种行为被称为进程切换c;任务切换或者是上下文切换。
尽管每个进程都可以拥有自己的地址空间c;但所有的进程必须共享CPU的寄存器!因此在恢复一个进程的执行以前内核必须确保每个寄存器装入了挂起进程时的值。进程恢复至执行前必须装入寄存器的一组数据被称为硬件上下文
硬件上下文是进程可执行上下文的一个子集c;因为可执行上下文包含进程执行时需要的所有信息。Linux 2.6使用软件执行线程切换:通过一组move指令逐步执行切换c;这样能较好地控制所装入数据的合法性。尤其是这时检查ds和es段寄存器的值成为可能
任务状态段TSS用来存放硬件上下文c;尽管Linux并不使用硬件上下文切换c;但它强制为每个系统中每个不同的CPU创建一个TSS。这么做的理由有:当8086的一个CPU从用户态切换到内核态时它就从TSS中获取内核态堆栈的地址c;当用户态进程试图通过IN或OUT指令访问一个io端口时c;CPU需要访问存放在TSS中的io许可全位图c;以检查该进程是否有访问端口的能力!
执行进程切换进程切换只能发生在精心定义的点:schedule函数。这里我们仅关心内核如何执行一个进程切换//从本质上来说每个进程切换有两步组成:
切换页全局目录已安装一个新的地址空间。
切换内核态堆栈和硬件上下文
因为硬件上下文提供了内核执行新进程所需要的所有信息c;包括CPU的寄存器!
进程切换的第二步由switch_to宏执行。它是内核中与硬件关系最密切的历程之一。首先该宏有三个参数它们是prevc;next和last。在任何进程切换中涉及到三个进程而不是两个。因为假设内核正决定战役进程A而激活Bc;在schedule函数中Prev指向a的描述符而next指向B的描述符。一旦是a暂停c;a的执行流就被冻结了。随后当内核再次想激活a时c;又必须暂停另一个进程C!这通常不同于B(你想想B可能也被切换走了)于是就要用prev指向Cc;而next选项a来执行另一个switch to宏。当a恢复它的执行流时就会找到他原来的内核栈。于是prev局部变量还是指向a的描述符。next指向B的描述符。此时代表a的执行的内核就失去了对C的任何作用c;但是事实表明这个引用对于完成进程切换还是很有用!
switch_to宏最后一个参数表示输出参数c;它表示宏把进程C的描述地址写在了什么位置
下面来简述简述汇编语言是如何实现进程切换的:
在EAX和EDX寄存器中分别保存prev和next的值
movl prev, %eax movl next, %edx
把eflags和ebp寄存器的内容保存在prev内核栈中c;必须保存他们的原因是编译器认为在switch to结束之前他们的值是保持不变的!
pushfl pushl %ebp
把ESP的内容保存到prev指向的thread.esp中c;使该字段指向prev内核栈的栈顶
movl %esp, 484(%eax)
把next指向thread.esp装入ESPc;此时内核开始在next的内核栈上操作!因此这条指令实际上完成了从prev项next的切换
mov 484(%edx), %esp
把标记为一的地址存入prev->thread.eip。当被替换的进程重新执行时c;进程执行被标记为一的那条指令。
movl $1f, 480(%eax)
宏把next thread.eip的值压入next的内核栈:
pushl 480(%edx)
跳到<code>__switch_to()code>C函数c;(这里就是主要完成硬件上下文切换,更新TSSc;我们不讲)
jmp __switch_to
这里被进程B替换的进程A再次获得CPU。它执行一些保存eflags和ebp寄存器内容的指令c;这里两条指令的第一条被标记为1
1: popl %ebppopfl
注意这些pop指令是如何引用prev进程的内核栈的!当进程调度程序选择prev作为进程c;在CPU上运行时将执行这些指令!
拷贝寄存器的内容到switch_to宏的第三个参数last标识的内存区域中:
movl %eax, last
Unix操作系统仅仅依赖进程创建来满足用户的需求现代Unix内核引入了三种不同的机制来加快进程创建和减轻进程创建的开销写时复制技术允许父子进程读取相同的物理也只有两者中的有一个试图写一个物理页内核就把这个页拷贝到一个新的物理页并且把新的物理页分配给正在写的进程轻量级进程允许父子之间共享每进程之间的内河的很多数据结构比如说列表打开文件表和信号处理微fork系统调用创建的进程能共享其父进程的内存地址空间为了防止父进程重写子进程需要的数据需要阻塞父进程的执行一直到子进程退出或执行一个新的程序为止但是这个调用基本上不被提倡
在Linux在中c;我们使用clone来创建轻量级的进程c;这是一些常见的标志
CLONE_NEWNS:使新进程拥有一个新的、独立的挂载命名空间c;可以隔离文件系统。 CLONE_NEWUTS:使新进程拥有一个新的、独立的 UTS 命名空间c;可以隔离主机名和域名。 CLONE_NEWIPC:使新进程拥有一个新的、独立的 IPC 命名空间c;可以隔离 System V IPC 和 POSIX 消息队列。 CLONE_NEWNET:使新进程拥有一个新的、独立的网络命名空间c;可以隔离网络设备、协议栈和端口。 CLONE_NEWPID:使新进程拥有一个新的、独立的 PID 命名空间c;可以隔离进程 ID。 CLONE_NEWUSER:使新进程拥有一个新的、独立的用户命名空间c;可以隔离用户和组 ID。 CLONE_FILES:使新进程共享打开的文件描述符表c;但不共享文件描述符的状态(例如文件偏移量)。 CLONE_FS:使新进程共享文件系统信息(例如当前工作目录和根目录)。 CLONE_VM:使新进程共享虚拟内存空间c;即在进程之间共享代码和数据段。 CLONE_SIGHAND:使新进程共享信号处理程序。 CLONE_THREAD:使新进程成为调用进程的线程c;与父进程共享进程 ID 和资源c;但拥有独立的栈。
clone, fork, vfork底层:
它执行以下这些步骤
通过查找pidmap_array位图c;为子进程分配新的PID
检查父进程的ptrace字段(current->ptrace):如果它的值不等于0c;说明有另外一个进程正在跟踪父进程c;因而c;do_fork()检查debugger程序是否自己想跟踪子进程(独立于由父进程指定的CLONE_PTRACE标志的值)。在这种情况下c;如果子进程不是内核线程(CLONE_UNTRACED标志被清0)c;那么do_fork()函数设置CLONE_PTRACE标志。
调用copy_process()复制进程描述符。如果所有必须的资源都是可用的c;该函数返回刚创建的task_struct描述符的地址。这是创建过程的关键步骤c;将在do_fork()之后描述它。
如果设置了CLONE_STOPPED标志c;或者必须跟踪子进程c;即在p->ptrace中设置了PT_PTRACED标志c;那么子进程的状态被设置成TASK_STOPPEDc;并为子进程增加挂起的SIGSTOP信号。在另外一个进程(不妨假设是跟踪进程或是父进程)把子进程的状态恢复为TASK_RUNNING之前(通常是通过发送SIGCONT信号)c;子进程将一直保持TASK_STOPPED状态。
如果没有设置CLONE_STOPPED标志c;则调用wake_up_new_task()函数以执行下述操作:
a.调整父进程和子进程的调度参数
b.如果子进程将和父进程运行在同一个CPU上(当内核创建一个新进程时父进程有可能会被转移到另一个CPU上执行)c;而且父进程和子进程不能共享同一组页表(CLONE_VM标志被清0)c;那么c;就把子进程插入父进程运行队列c;插入时让子进程恰好在父进程前面c;因此而迫使子进程先于父进程运行。如果子进程刷新其地址空间c;并在创建之后执行新程序c;那么这种简单的处理会产生较好的性能。而如果我们让父进程先运行c;那么写时复制机制将会执行一系列不必要的页面复制。
c.否则c;如果子进程与父进程运行在不同的CPU上c;或者父进程和子进程共享同一组页表(CLONE_VM标志被置位)c;就把子进程插入父进程运行队列的队尾。
如果CLONE_STOPPED标志被置位c;则把子进程置为TASK_STOPPED状态。
如果父进程被跟踪c;则把子进程的PID存入current的ptrace_message字段并调用ptrace_notify()。ptrace_notify()是当前进程停止运行c;并向当前进程的父进程发送SIGCHLD信号。子进程的祖父进程是跟踪父进程的debugger进程。SIGCHLD信号通知debugger进程:current已经创建了一个子进程c;可以通过查找current->ptrace_message字段获得子进程的PID。
如果设置了CLONE_VFORK标志c;则把父进程插入等待队列c;并挂起父进程直到子进程释放自己的内存地址空间(也就是说c;直到子进程结束或执行新的程序)。
结束并返回子进程的PID。
欸c;来看看中间的<code>copy_processcode>
CLONE_NEWNS和CLONE_FS同时被设置。前者标志表示子进程需要自己的命名空间c;而后者标志则代表子进程共享父进程的根目录和当前工作目录c;两者不可兼容。在传统的Unix系统中c;整个系统只有一个已经安装的文件系统树。每个进程从系统的根文件系统开始c;通过合法的路径可以访问任何文件。在2.6版本中的内核中c;每个进程都可以拥有属于自己的已安装文件系统树c;也被称为命名空间。通常大多数进程都共享init进程所使用的已安装文件系统树c;只有在clone_flags中设置了CLONE_NEWNS标志时c;才会为此新进程开辟一个新的命名空间。
CLONE_THREAD被设置c;但CLONE_SIGHAND未被设置。如果子进程和父进程属于同一个线程组(CLONE_THREAD被设置)c;那么子进程必须共享父进程的信号(CLONE_SIGHAND被设置)。
CLONE_SIGHAND被设置c;但CLONE_VM未被设置。如果子进程共享父进程的信号c;那么必须同时共享父进程的内存描述符和所有的页表(CLONE_VM被设置)。
安全性检查。通过调用security_task_create()和后面的security_task_alloc()执行所有附加的安全性检查。询问 Linux Security Module (LSM) 看当前任务是否可以创建一个新任务。LSM是SELinux的核心。 复制进程描述符。通过dup_task_struct()为子进程分配一个内核栈、thread_info结构和task_struct结构。注意c;这里将当前进程描述符指针作为参数传递到此函数中。
首先c;该函数分别定义了指向task_struct和thread_info结构体的指针。接着c;prepare_to_copy为正式的分配进程描述符做一些准备工作。主要是将一些必要的寄存器的值保存到父进程的thread_info结构中。这些值会在稍后被复制到子进程的thread_info结构中。执行alloc_task_struct宏c;该宏负责为子进程的进程描述符分配空间c;将该片内存的首地址赋值给tskc;随后检查这片内存是否分配正确。执行alloc_thread_info宏c;为子进程获取一块空闲的内存区c;用来存放子进程的内核栈和thread_info结构c;并将此会内存区的首地址赋值给ti变量c;随后检查是否分配正确。
上面已经说明过orig是传进来的current宏c;指向当前进程描述符的指针。arch_dup_task_struct直接将orig指向的当前进程描述符内容复制到当前里程描述符tsk。接着c;用atomic_set将子进程描述符的使用计数器设置为2c;表示该进程描述符正在被使用并且处于活动状态。最后返回指向刚刚创建的子进程描述符内存区的指针。
通过dup_task_struct可以看到c;当这个函数成功操作之后c;子进程和父进程的描述符中的内容是完全相同的。在稍后的copy_process代码中c;我们将会看到子进程逐步与父进程区分开来。
一些初始化。通过诸如ftrace_graph_init_taskc;rt_mutex_init_task完成某些数据结构的初始化。调用copy_creds()复制证书(应该是复制权限及身份信息)。
检测系统中进程的总数量是否超过了max_threads所规定的进程最大数。
复制标志。通过copy_flagsc;将从do_fork()传递来的的clone_flags和pid分别赋值给子进程描述符中的对应字段。
初始化子进程描述符。初始化其中的各个字段c;使得子进程和父进程逐渐区别出来。这部分工作包含初始化子进程中的children和sibling等队列头、初始化自旋锁和信号处理、初始化进程统计信息、初始化POSIX时钟、初始化调度相关的统计信息、初始化审计信息。它在copy_process函数中占据了相当长的一段的代码c;不过考虑到task_struct结构本身的复杂性c;也就不足为奇了。
调度器设置。调用sched_fork函数执行调度器相关的设置c;为这个新进程分配CPUc;使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占。并且c;为了不对其他进程的调度产生影响c;此时子进程共享父进程的时间片。
复制进程的所有信息。根据clone_flags的具体取值来为子进程拷贝或共享父进程的某些数据结构。比如copy_semundo()、复制开放文件描述符(copy_files)、复制符号信息(copy_sighand 和 copy_signal)、复制进程内存(copy_mm)以及最终复制线程(copy_thread)。
复制线程。通过copy_threads()函数更新子进程的内核栈和寄存器中的值。在之前的dup_task_struct()中只是为子进程创建一个内核栈c;至此才是真正的赋予它有意义的值。
当父进程发出clone系统调用时c;内核会将那个时候CPU中寄存器的值保存在父进程的内核栈中。这里就是使用父进程内核栈中的值来更新子进程寄存器中的值。特别的c;内核将子进程eax寄存器中的值强制赋值为0c;这也就是为什么使用fork()时子进程返回值是0。而在do_fork函数中则返回的是子进程的pidc;这一点在上述内容中我们已经有所分析。另外c;子进程的对应的thread_info结构中的esp字段会被初始化为子进程内核栈的基址。
分配pid。用alloc_pid函数为这个新进程分配一个pidc;Linux系统内的pid是循环使用的c;采用位图方式来管理。简单的说c;就是用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后c;判断pid是否分配成功。成功则赋给p->pid。
更新属性和进程数量。根据clone_flags的值继续更新子进程的某些属性。将 nr_threads加一c;表明新进程已经被加入到进程集合中。将total_forks加一c;以记录被创建进程数量。
如果上述过程中某一步出现了错误c;则通过goto语句跳到相应的错误代码处;如果成功执行完毕c;则返回子进程的描述符p。
所有进程的祖先是0进程c;他在初始化阶段从无到有的创建一个进程。进程1是init进程!
进程的终止有两个:exitt_group来终结一个进程组c;exit系统调用终结摸一个线程
这个函数用来杀死属于current进程组的所有进程!它接收进程终止代号作为参数
首先它检查退出进程的signal_group_exit标志是否不为零c;如果不为零c;说明内核已经开始为进程组执行退出的过程!在这种情况下就把存放在<code>current->signal->group->exit_codecode>中的值当做退出码c;然后跳到第四步
否则设置进程的signal group exit标志并把中指戴好存放在<code>current->signal->group->exit_codecode>字段调用
zap_other_threads函数杀死杀死current进程组的其他进程
调用do_exit函数c;把进程的终止代号传递给他
做这些事情:
把进程描述符的flag字段设置为PF_EXITING标志c;以表示进程正在被删除。
分别调用exit_mm()、exit_sem()、__exit_files()、__exit_fs()、exit_namespace()和exit_thread()函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及I/O权限位图相关的数据结构。如果没有其它进程共享这些数据结构c;那么这些函数还删除所有这些数据结构中。
如果实现了被杀死进程的执行域和可执行格式的内核函数包含在内核模块中c;则函数递减它们的使用计数器。
把进程描述符的exit_code字段设置成进程的终止代号c;这个值要么是_exit()或exit_group()系统调用参数c;要么是由内核提供的一个错误代码。
调用exit_notify()函数执行下面的操作:
更新父进程和子进程的亲属关系。如果同一线程组中有正在运行的进程c;就让终止进程所创建的所有子进程都变成同一线程组中另外一个进程的子进程c;否则让它们成为init的子进程
检查被终止进程其进程描述符的exit_signal字段是否不等于-1c;并检查进程是否是其所属进程组的最后一个成员。在这种情况下c;函数通过给正被终止进程的父进程发送一个信号c;以通知父进程子进程死亡。
否则c;也就是exit_signal字段等于-1c;或者线程组中还有其它进程c;那么只要进程正在被跟踪c;就向父进程发送一个SIGCHLD信号。
如果进程描述符的exit_signal字段等于-1c;而且进程没有被跟踪c;就把进程描述符的exit_state字段置为EXIT_DEADc;然后调用release_task()回收进程的其它数据结构占用的内存c;并递减进程描述符的使用计数器c;以使进程描述符本身正好不会被释放。
否则c;如果进程描述符的exit_signal字段不等于-1c;或进程正在被跟踪c;就把exit_state字段置为EXIT_ZOMBIE。
把进程描述符的flags字段设置为PF_DEAD标志。
Unix/Linux编程:进程资源限制_如何限制一个进程能够使用的线程等资源-CSDN博客