Linux深入学习 - 进程

目录

进程描述符

标识一个进程

进程组织

进程之间的关系

如何组织进程

等待队列

进程资源限制

进程切换

硬件上下文

switch_to宏

创建进程

do_fork

内核进程

撤销有一个进程

do_group_exit

do_exit函数

Reference


下面开始讨论一个非常重要的抽象:这在我们前边已经提到过了的!也就是进程!

进程在使用中常有几个不同的含义,比如说在一般的操作系统教科书中给出了通用定义是:

进程是程序执行的一个实例

进程就像生命一般他们被产生,有或多或少的有效生命周期,,可以产生一个或者多个子进程,但是它们最终都会死亡!从内核观点上看进程的目的就是分配系统资源的一个实体

当一个进程创建时,它几乎与父进程相同。它接收父进程地址空间的一个逻辑拷贝,并开始执行父进程相同的代码!

Linux使用轻量级进程,对多线程应用程序提供了更好的支持。两个轻量级进程基本上可以共享一些资源诸如打开的文件,地址空间等。只要其中一个修改了共享资源,另一个就立即查看各种修改

进程描述符

为了管理进程,内核必须对每个进程所做的事情有清楚的描述!

首先我们要看的是进程的状态:顾名思义当我们查看源码时,进程描述符中的state字段就描述了进程当前的状态。它由一组标志组成。有以下几种可能的状态:

  • 可运行状态(TASK_RUNNING):其进程要么在CPU上执行要么准备执行与

  • 可中断的等待状态(TASK_INTERRUPTIBLE):进程被挂起直到某个条件为真,产生一个硬件中断,释放进程,正等待的系统资源或传递一个信号都是可以唤醒进程的条件

  • 不可中断的等待状态(TASK_UNINTERRUPTIBLE):与可中断的等待状态类似,但是有一个例外:把信号传递到睡眠进程不能改变它的状态。这种状态很少用到,但在一些特定的情况下,也就是要求进程必须等待直到一个不能被中断的事件发生时,这种状态很有用!例如当进程打开一个设备文件,设备驱动程序开始探测相应的硬件设备时会用到这个状态,探测完成以前设备驱动程序是不可以被中断的!否则硬件设备会处于一种不可预知的状态!

  • 暂停状态(TASK_STOPPED):进程的执行被暂停,当进程接收到这四个信号:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU,进入暂停状态

  • 跟踪状态(TASK_TRACED):进程的执行已经被调试程序暂停,当一个进程被另一个进程监控时,任何信号都可以把这个进程置于这个状态!

还有两个进程状态,是既可以存放在进程描述符当中,,也可以存放在退出状态字段当中!

  • 僵死状态:进程的执行被终止,但是父进程并没有发布wait4或wait pid系统调用来返回有关死亡进程的信息

  • 将死撤销状态:最终状态由于父进程发出了上面提到的两个系统调用,因而进程由系统进行删除。为了防止其他执行进程在同一进程上也执行wait类系统调用而把进程的状态由僵死状态设为僵死撤销状态!

标识一个进程

一般来说能被独立调度的每个进程上下文都必须有它自己的进程描述符!也就是pid。pid存放在进程描述符的pid字段中,它被顺序编号。内核使用一个pid_map_array位图来表示当前已经被分配的pid号和闲置的pid号。

进程描述符处理进程是动态实体,因此内核必须能够同时处理很多进程!并把进程描述符存放在动态内存中,而不是永久分配给内核的内存区!

标识当前进程是使用一个叫做thread_info的结构。进程最常用地址不是thread_info地址而是进程描述符的地址,为了获得当前CPU上运行形成的描述符指针内核需要调用current宏。该宏本质上等价于current thread info()->task

进程组织

Linux使用双向链表来管理进程!被称为进程链表。

task_running状态的进程列表,当内核寻找一个新进程在CPU上运行时,必须只考虑可以运行的。进程在Linux 2.6实现的运行队列与之前的实现有所不同,其目的是让调度程序能够在固定时间内选出最佳的可知运行进程!与队列中的可运行的进程数无关

进程之间的关系

程序创建的进程具有父子关系。如果一个进程创建多个子进程,则多个子进程之间又有兄弟关系。进程描述符中表示进程亲属关系的字段描述如下:

描述符 说明
real parent 指向创建了P的进程的描述符,如果P的进程不再存在就指向进程init进程描述符
parent P的当前父进程,它的值通常与real parent一致。但偶尔有所不同,比如说另一个进程发出监控P的Ptrace调用时。
Children 链表的头部链表中的所有元素都是P创建的子进程
sibling 指向兄弟进程链表中的下一个元素,或前一个元素的指针。这些兄弟进程的父进程都是P

如何组织进程

运行队列链表把处于task running状态的所有进程组织在,一起不同的状态要求不同的处理!Linux选择下面的方式之一:

没有未处理task_stoppedexit_zombie或者是exit_dead状态的进程建立专门的链表。由于对处于暂停僵死,死亡状态进程的访问比较简单,或者通过pid或者通过特定父进程的子进程链表。所以不必对这三种状态进行分组

根据不同的特殊事件把处于task_interrupttabletask_uninterruptable状态的进程细分为许多类,每个类都对应一个特殊事件。

在这种情况下进程状态提供的信息满足不了快速检索进程的需要,所以需要另外引入进程列表!

等待队列

等待队列在内核中有很多用途!特别是在处理中断处理进程同步及定时(进程必须经常等待某些事情的发生------等待队列是由双向链表所实现的!关于具体操作可以参看相关的博客。

进程资源限制

每一个进程都有资源限制:

复制代码
struct rlimit {
               rlim_t rlim_cur;  /* Soft limit */
               rlim_t rlim_max;  /* Hard limit (ceiling for rlim_cur) */
           };

这是常见的一些限制

  • RLIMIT_AS

    • 进程虚拟内存限制大小(字节数),即进程总的可用存储空间的最大长度

    • yixie试图(brk()、sbrk()、mmap()、mremap()以及 shmat())超出这个限制会得到 ENOMEM 错误。

    • 在实践中,程序中会超出这个限制的最常见的地方是在调用 malloc 包中的函数时,因为它们会使用 sbrk()和mmap()。当碰到这个限制时,栈增长操作也会失败,进而会出现下面 RLIMIT_STACK 限制中列出的情况。

  • RLIMIT_CORE

    • 核心文件大小(字节数),即core文件的最大字节数

    • 当达到这个限制时,核心 dump 文件就不会再产生了

    • 如果值为0标识阻止创建core文件

      • 这种做法有时候是比较有用的,因为核心 dump 文件可能会变得非常大,而最终用户通常又不知道如何处理这些文件。

      • 另一个禁用核心 dump文件的原因是安全性------防止程序占用的内存中的内容输出到磁盘上。

      • 如果 RLIMIT_FSIZE限制值低于这个限制值,那么核心 dump 文件的最大大小会被限制为 RLIMIT_FSIZE 字节。

  • RLIMIT_CPU

    • 进程最多使用的 CPU 时间(包括系统模式和用户模式)。

    • 当超过此软限制时,向该进程发送SIGXCPU信号(SIGXCPU 信号的默认动作是终止一个进程并输出一个核心 dump。此外,也可以捕获这个信号并将控制返回给主程序。)。

    • 不同的 UNIX 实现对进程处理完 SIGXCPU 信号之后继续消耗 CPU 时间这种情况的处理方式不同。大多数会每隔固定时间间隔向进程发送一个 SIGXCPU 信号。

    • 在达到软限制值之后,Linux 内核会在进程每消耗一秒钟的 CPU 时间后向其发送一个 SIGXCPU 信号。当进程持续执行直至达到硬 CPU 限制时,内核会向其发送一个 SIGKILL 信号,该信号总是会终止进程。

  • RLIMIT_DATA

    • 数据段的最大字节长度。这是初始化数据段、非初始化数据段、堆的总和

    • 试图(sbrk()和 brk())访问这个限制之外的数据段会得到ENOMEM 的错误。

    • 与 RLIMIT_AS 一样,程序中会超出这个限制的最常见的地方是在调用malloc 包中的函数时。

  • RLIMIT_FSIZE

    • 文件大小(字节数),即可以创建的文件的最大字节长度

    • 当超过此软限制时,向该进程发送SIGXFSZ信号。并且系统调用(如 write()或truncate())会返回EFBIG错误。

    • SIGXFSZ信号的默认动作是终止进程并产生一个核心dump。此外,也可以捕获这个信号并将控制返回给主程序。不管怎样,后续视图扩充该文件的操作都会得到同样的信号和错误。

  • RLIMIT_MEMLOCK

    • 一个进程最多能够将多少字节的虚拟内存锁进物理内存以防止内存被交换出去

    • 这个限制会影响 mlock()和 mlockall()系统调用以及 mmap()和 shmctl()系统调用的加锁参数

    • 如果在调用 mlockall()时指定了 MCL_FUTURE 标记,那么 RLIMIT_MEMLOCK 限制也会导致后续的 brk()、sbrk()、mmap()和 mremap()调用失败

  • RLIMIT_MSGQUEUE

    • 能够为调用进程的真实用户 ID 的 POSIX 消息队列分配的最大字节数。

    • RLIMIT_MSGQUEUE 限制只会影响调用进程。这个用户下的其他进程不会受到影响,因为它们也会设置这个限制或继承这个限制。

  • RLIMIT_NICE

    • 规定了使用 sched_setscheduler()和 nice()能够为进程设置的最大 nice 值。

    • 这个最大值是通过公式 20 -- rlim_cur 计算得来的,其中 rlim_cur 是当前的 RLIMIT_NICE 软资源限制

  • RLIMIT_NOFILE

    • 一个进程能够分配的最大文件描述符数量加 1

    • 试图(如 open()、pipe()、socket()、accept()、shm_open()、dup()、dup2()、fcntl(F_DUPFD)和 epoll_create())分配的文件描述符数量超出这个限制时会失败。

      • 在大多数情况,失败的错误是 EMFILE

      • 在 dup2(fd, newfd)调用中,失败的错误是 EBADF,

      • 在 fcntl(fd, F_DUPFD, newfd)调用中当 newfd 大于或等于这个限制时,失败的错误是 EINVAL。

    • 更改此限制将影响到sysconf函数在参数_SC_OPEN_MAX中返回的值

    • 在 Linux 上可以通过使用 readdir()扫描/proc/PID/fd 目录下的内容来检查一个进程当前打开的文件描述符,这个目录包含了进程当前打开的每个文件描述符的符号链接。

    • 从 2.6.25 的版本开始,这个限制由 Linux 特有的/proc/sys/fs/nr_open 文件定义。这个文件中的默认值是 1048576,超级用户可以修改这个值。试图将软或硬 RLIMIT_NOFILE 限制设置为一个大于最大值的值会产生 EPERM 错误。

    • 还存在一个系统级别的限制,它规定了系统中所有进程能够打开的文件数量,通过 Linux 特有的/proc/sys/fs/file-max 文件能够获取和修改这个限制。

      • 只有特权(CAP_SYS_ADMIN)进程才能够超出 file-max 的限制。

      • 在非特权进程中,当系统调用碰到 file-max 限制时会返回 ENFILE 错误

  • RLIMIT_NPROC

    • 规定了调用进程的真实用户 ID 下最多能够创建的进程数量。

    • 试图(fork()、vfork()和 clone())超出这个限制会得到 EAGAIN 错误

    • RLIMIT_NPROC 限制只影响调用进程。这个用户下的其他进程不会受到影响,除非它们也设置或继承了这个限制。这个限制不适用于特权(CAP_SYS_ADMIN 和 CAP_SYS_RESOURCE)进程。

    • Linux 还提供了系统层面的限制来规定所有用户能够创建的进程数量。在 Linux 2.4以及之后的版本中,可以使用 Linux 特有的/proc/sys/kernel/threads-max 文件来获取和修改这个限制

    • 准确地说,RLIMIT_NPROC 资源限制和 threads-max 文件实际上限制的是所能创建的线程数量,而不是进程的数量

    • 更改此限制将影响到sysconf函数在参数_SC_CHILD_MAX中返回的值

    • 不存在一种统一的方法能够在不同系统中找出某个特定用户 ID 已经创建的进程数。

  • RLIMIT_RSS

    • 进程驻留集中的最大页面数,即当前位于物理内存中的虚拟内存页面总数。

    • Linux 提供了这个限制,但当前并没有起任何作用

    • 如果可用的物理存储器非常少,则内核将从进程处取同超过RSS的部分

  • RLIMIT_RTPRIO

    • 规定了使用 sched_ setscheduler()和 sched_setparam()能够为进程设置的最高实时优先级(自 Linux 2.6.12 起)
  • RLIMIT_RTTIME

    • 规定了一个进程在实时调度策略中不睡眠(即执行一个阻塞系统调用)的情况下最大能消耗的 CPU 秒数(微秒;自 Linux 2.6.25 起)

    • 如果进程达到了软限制,那么内核会向进程发送一个 SIGXCPU 信号,之后进程每消耗一秒的 CPU 时间都会收到一个SIGXCPU 信号。在达到硬限制时,内核会向进程发送一个 SIGKILL 信号。

  • RLIMIT_SBSIZE

    • 在任一给定时刻,一个用户可以占用的套接字缓冲区的最大长度(字节)
  • RLIMIT_SIGPENDING - 一个进程可排队的信号最大数量制(Linux 特有的,自 Linux 2.6.8 起) - 试图(sigqueue())超出这个限制会得到 EAGAIN错误。 - RLIMIT_SIGPENDING 只影响调用进程。这个用户下的其他进程不会受到影响,除非它们也设置或继承了这个限制

  • RLIMIT_STACK

    • 栈段的大小(字节数)

    • 试图扩展栈大小以至于超出这个限制会导致内核向该进程发送一个 SIGSEGV 信号。

    • 由于栈空间已经被用光了,因此捕获这个信号的唯一方式是建立另外一个备用的信号栈,

  • RLIMIT_VMEN

    • 这时RLIMIT_AS的同义词

进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程!以恢复以前挂起的某个进程的执行。这种行为被称为进程切换,任务切换或者是上下文切换。

硬件上下文

尽管每个进程都可以拥有自己的地址空间,但所有的进程必须共享CPU的寄存器!因此在恢复一个进程的执行以前内核必须确保每个寄存器装入了挂起进程时的值。进程恢复至执行前必须装入寄存器的一组数据被称为硬件上下文

硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。Linux 2.6使用软件执行线程切换:通过一组move指令逐步执行切换,这样能较好地控制所装入数据的合法性。尤其是这时检查ds和es段寄存器的值成为可能

任务状态段TSS用来存放硬件上下文,尽管Linux并不使用硬件上下文切换,但它强制为每个系统中每个不同的CPU创建一个TSS。这么做的理由有:当8086的一个CPU从用户态切换到内核态时它就从TSS中获取内核态堆栈的地址,当用户态进程试图通过IN或OUT指令访问一个io端口时,CPU需要访问存放在TSS中的io许可全位图,以检查该进程是否有访问端口的能力!

执行进程切换进程切换只能发生在精心定义的点:schedule函数。这里我们仅关心内核如何执行一个进程切换//从本质上来说每个进程切换有两步组成:

切换页全局目录已安装一个新的地址空间。

切换内核态堆栈和硬件上下文

因为硬件上下文提供了内核执行新进程所需要的所有信息,包括CPU的寄存器!

switch_to宏

进程切换的第二步由switch_to宏执行。它是内核中与硬件关系最密切的历程之一。首先该宏有三个参数它们是prev,next和last。在任何进程切换中涉及到三个进程而不是两个。因为假设内核正决定战役进程A而激活B,在schedule函数中Prev指向a的描述符而next指向B的描述符。一旦是a暂停,a的执行流就被冻结了。随后当内核再次想激活a时,又必须暂停另一个进程C!这通常不同于B(你想想B可能也被切换走了)于是就要用prev指向C,而next选项a来执行另一个switch to宏。当a恢复它的执行流时就会找到他原来的内核栈。于是prev局部变量还是指向a的描述符。next指向B的描述符。此时代表a的执行的内核就失去了对C的任何作用,但是事实表明这个引用对于完成进程切换还是很有用!

switch_to宏最后一个参数表示输出参数,它表示宏把进程C的描述地址写在了什么位置

下面来简述简述汇编语言是如何实现进程切换的:

  • 在EAX和EDX寄存器中分别保存prev和next的值
复制代码
movl prev, %eax
movl next, %edx

把eflags和ebp寄存器的内容保存在prev内核栈中,必须保存他们的原因是编译器认为在switch to结束之前他们的值是保持不变的!

复制代码
pushfl
pushl %ebp

把ESP的内容保存到prev指向的thread.esp中,使该字段指向prev内核栈的栈顶

复制代码
movl %esp, 484(%eax)

把next指向thread.esp装入ESP,此时内核开始在next的内核栈上操作!因此这条指令实际上完成了从prev项next的切换

复制代码
mov 484(%edx), %esp

把标记为一的地址存入prev->thread.eip。当被替换的进程重新执行时,进程执行被标记为一的那条指令。

复制代码
movl $1f, 480(%eax)

宏把next thread.eip的值压入next的内核栈:

复制代码
pushl 480(%edx)

跳到__switch_to()C函数,(这里就是主要完成硬件上下文切换,更新TSS,我们不讲)

复制代码
jmp __switch_to

这里被进程B替换的进程A再次获得CPU。它执行一些保存eflags和ebp寄存器内容的指令,这里两条指令的第一条被标记为1

复制代码
1: 
    popl %ebp
    popfl

注意这些pop指令是如何引用prev进程的内核栈的!当进程调度程序选择prev作为进程,在CPU上运行时将执行这些指令!

拷贝寄存器的内容到switch_to宏的第三个参数last标识的内存区域中:

复制代码
movl %eax, last

创建进程

Unix操作系统仅仅依赖进程创建来满足用户的需求现代Unix内核引入了三种不同的机制来加快进程创建和减轻进程创建的开销写时复制技术允许父子进程读取相同的物理也只有两者中的有一个试图写一个物理页内核就把这个页拷贝到一个新的物理页并且把新的物理页分配给正在写的进程轻量级进程允许父子之间共享每进程之间的内河的很多数据结构比如说列表打开文件表和信号处理微fork系统调用创建的进程能共享其父进程的内存地址空间为了防止父进程重写子进程需要的数据需要阻塞父进程的执行一直到子进程退出或执行一个新的程序为止但是这个调用基本上不被提倡

在Linux在中,我们使用clone来创建轻量级的进程,这是一些常见的标志

复制代码
CLONE_NEWNS:使新进程拥有一个新的、独立的挂载命名空间,可以隔离文件系统。
CLONE_NEWUTS:使新进程拥有一个新的、独立的 UTS 命名空间,可以隔离主机名和域名。
CLONE_NEWIPC:使新进程拥有一个新的、独立的 IPC 命名空间,可以隔离 System V IPC 和 POSIX 消息队列。
CLONE_NEWNET:使新进程拥有一个新的、独立的网络命名空间,可以隔离网络设备、协议栈和端口。
CLONE_NEWPID:使新进程拥有一个新的、独立的 PID 命名空间,可以隔离进程 ID。
CLONE_NEWUSER:使新进程拥有一个新的、独立的用户命名空间,可以隔离用户和组 ID。
CLONE_FILES:使新进程共享打开的文件描述符表,但不共享文件描述符的状态(例如文件偏移量)。
CLONE_FS:使新进程共享文件系统信息(例如当前工作目录和根目录)。
CLONE_VM:使新进程共享虚拟内存空间,即在进程之间共享代码和数据段。
CLONE_SIGHAND:使新进程共享信号处理程序。
CLONE_THREAD:使新进程成为调用进程的线程,与父进程共享进程 ID 和资源,但拥有独立的栈。

do_fork

clone, fork, vfork底层:

它执行以下这些步骤

  • 通过查找pidmap_array位图,为子进程分配新的PID

  • 检查父进程的ptrace字段(current->ptrace):如果它的值不等于0,说明有另外一个进程正在跟踪父进程,因而,do_fork()检查debugger程序是否自己想跟踪子进程(独立于由父进程指定的CLONE_PTRACE标志的值)。在这种情况下,如果子进程不是内核线程(CLONE_UNTRACED标志被清0),那么do_fork()函数设置CLONE_PTRACE标志。

  • 调用copy_process()复制进程描述符。如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址。这是创建过程的关键步骤,将在do_fork()之后描述它。

  • 如果设置了CLONE_STOPPED标志,或者必须跟踪子进程,即在p->ptrace中设置了PT_PTRACED标志,那么子进程的状态被设置成TASK_STOPPED,并为子进程增加挂起的SIGSTOP信号。在另外一个进程(不妨假设是跟踪进程或是父进程)把子进程的状态恢复为TASK_RUNNING之前(通常是通过发送SIGCONT信号),子进程将一直保持TASK_STOPPED状态。

  • 如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task()函数以执行下述操作:

a.调整父进程和子进程的调度参数

b.如果子进程将和父进程运行在同一个CPU上(当内核创建一个新进程时父进程有可能会被转移到另一个CPU上执行),而且父进程和子进程不能共享同一组页表(CLONE_VM标志被清0),那么,就把子进程插入父进程运行队列,插入时让子进程恰好在父进程前面,因此而迫使子进程先于父进程运行。如果子进程刷新其地址空间,并在创建之后执行新程序,那么这种简单的处理会产生较好的性能。而如果我们让父进程先运行,那么写时复制机制将会执行一系列不必要的页面复制。

c.否则,如果子进程与父进程运行在不同的CPU上,或者父进程和子进程共享同一组页表(CLONE_VM标志被置位),就把子进程插入父进程运行队列的队尾。

  • 如果CLONE_STOPPED标志被置位,则把子进程置为TASK_STOPPED状态。

  • 如果父进程被跟踪,则把子进程的PID存入current的ptrace_message字段并调用ptrace_notify()。ptrace_notify()是当前进程停止运行,并向当前进程的父进程发送SIGCHLD信号。子进程的祖父进程是跟踪父进程的debugger进程。SIGCHLD信号通知debugger进程:current已经创建了一个子进程,可以通过查找current->ptrace_message字段获得子进程的PID。

  • 如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间(也就是说,直到子进程结束或执行新的程序)。

  • 结束并返回子进程的PID。

欸,来看看中间的copy_process

  • 定义返回值亦是retval和新的进程描述符task_struct结构p。

  • 标志合法性检查。对clone_flags所传递的标志组合进行合法性检查。当出现以下三种情况时,返回出错代号:

    • CLONE_NEWNS和CLONE_FS同时被设置。前者标志表示子进程需要自己的命名空间,而后者标志则代表子进程共享父进程的根目录和当前工作目录,两者不可兼容。在传统的Unix系统中,整个系统只有一个已经安装的文件系统树。每个进程从系统的根文件系统开始,通过合法的路径可以访问任何文件。在2.6版本中的内核中,每个进程都可以拥有属于自己的已安装文件系统树,也被称为命名空间。通常大多数进程都共享init进程所使用的已安装文件系统树,只有在clone_flags中设置了CLONE_NEWNS标志时,才会为此新进程开辟一个新的命名空间。

    • CLONE_THREAD被设置,但CLONE_SIGHAND未被设置。如果子进程和父进程属于同一个线程组(CLONE_THREAD被设置),那么子进程必须共享父进程的信号(CLONE_SIGHAND被设置)。

    • CLONE_SIGHAND被设置,但CLONE_VM未被设置。如果子进程共享父进程的信号,那么必须同时共享父进程的内存描述符和所有的页表(CLONE_VM被设置)。

  • 安全性检查。通过调用security_task_create()和后面的security_task_alloc()执行所有附加的安全性检查。询问 Linux Security Module (LSM) 看当前任务是否可以创建一个新任务。LSM是SELinux的核心。 复制进程描述符。通过dup_task_struct()为子进程分配一个内核栈、thread_info结构和task_struct结构。注意,这里将当前进程描述符指针作为参数传递到此函数中。

  • 首先,该函数分别定义了指向task_struct和thread_info结构体的指针。接着,prepare_to_copy为正式的分配进程描述符做一些准备工作。主要是将一些必要的寄存器的值保存到父进程的thread_info结构中。这些值会在稍后被复制到子进程的thread_info结构中。执行alloc_task_struct宏,该宏负责为子进程的进程描述符分配空间,将该片内存的首地址赋值给tsk,随后检查这片内存是否分配正确。执行alloc_thread_info宏,为子进程获取一块空闲的内存区,用来存放子进程的内核栈和thread_info结构,并将此会内存区的首地址赋值给ti变量,随后检查是否分配正确。

  • 上面已经说明过orig是传进来的current宏,指向当前进程描述符的指针。arch_dup_task_struct直接将orig指向的当前进程描述符内容复制到当前里程描述符tsk。接着,用atomic_set将子进程描述符的使用计数器设置为2,表示该进程描述符正在被使用并且处于活动状态。最后返回指向刚刚创建的子进程描述符内存区的指针。

  • 通过dup_task_struct可以看到,当这个函数成功操作之后,子进程和父进程的描述符中的内容是完全相同的。在稍后的copy_process代码中,我们将会看到子进程逐步与父进程区分开来。

  • 一些初始化。通过诸如ftrace_graph_init_task,rt_mutex_init_task完成某些数据结构的初始化。调用copy_creds()复制证书(应该是复制权限及身份信息)。

  • 检测系统中进程的总数量是否超过了max_threads所规定的进程最大数。

  • 复制标志。通过copy_flags,将从do_fork()传递来的的clone_flags和pid分别赋值给子进程描述符中的对应字段。

  • 初始化子进程描述符。初始化其中的各个字段,使得子进程和父进程逐渐区别出来。这部分工作包含初始化子进程中的children和sibling等队列头、初始化自旋锁和信号处理、初始化进程统计信息、初始化POSIX时钟、初始化调度相关的统计信息、初始化审计信息。它在copy_process函数中占据了相当长的一段的代码,不过考虑到task_struct结构本身的复杂性,也就不足为奇了。

  • 调度器设置。调用sched_fork函数执行调度器相关的设置,为这个新进程分配CPU,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占。并且,为了不对其他进程的调度产生影响,此时子进程共享父进程的时间片。

  • 复制进程的所有信息。根据clone_flags的具体取值来为子进程拷贝或共享父进程的某些数据结构。比如copy_semundo()、复制开放文件描述符(copy_files)、复制符号信息(copy_sighand 和 copy_signal)、复制进程内存(copy_mm)以及最终复制线程(copy_thread)。

  • 复制线程。通过copy_threads()函数更新子进程的内核栈和寄存器中的值。在之前的dup_task_struct()中只是为子进程创建一个内核栈,至此才是真正的赋予它有意义的值。

  • 当父进程发出clone系统调用时,内核会将那个时候CPU中寄存器的值保存在父进程的内核栈中。这里就是使用父进程内核栈中的值来更新子进程寄存器中的值。特别的,内核将子进程eax寄存器中的值强制赋值为0,这也就是为什么使用fork()时子进程返回值是0。而在do_fork函数中则返回的是子进程的pid,这一点在上述内容中我们已经有所分析。另外,子进程的对应的thread_info结构中的esp字段会被初始化为子进程内核栈的基址。

  • 分配pid。用alloc_pid函数为这个新进程分配一个pid,Linux系统内的pid是循环使用的,采用位图方式来管理。简单的说,就是用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。成功则赋给p->pid。

  • 更新属性和进程数量。根据clone_flags的值继续更新子进程的某些属性。将 nr_threads加一,表明新进程已经被加入到进程集合中。将total_forks加一,以记录被创建进程数量。

  • 如果上述过程中某一步出现了错误,则通过goto语句跳到相应的错误代码处;如果成功执行完毕,则返回子进程的描述符p。

内核进程

所有进程的祖先是0进程,他在初始化阶段从无到有的创建一个进程。进程1是init进程!

撤销有一个进程

进程的终止有两个:exitt_group来终结一个进程组,exit系统调用终结摸一个线程

do_group_exit

这个函数用来杀死属于current进程组的所有进程!它接收进程终止代号作为参数

  • 首先它检查退出进程的signal_group_exit标志是否不为零,如果不为零,说明内核已经开始为进程组执行退出的过程!在这种情况下就把存放在current->signal->group->exit_code中的值当做退出码,然后跳到第四步

  • 否则设置进程的signal group exit标志并把中指戴好存放在current->signal->group->exit_code字段调用

  • zap_other_threads函数杀死杀死current进程组的其他进程

  • 调用do_exit函数,把进程的终止代号传递给他

do_exit函数

做这些事情:

  1. 把进程描述符的flag字段设置为PF_EXITING标志,以表示进程正在被删除。

  2. 如果需要,通过函数del_timer_sync()从动态定时器队列中删除进程描述符。

  3. 分别调用exit_mm()、exit_sem()、__exit_files()、__exit_fs()、exit_namespace()和exit_thread()函数从进程描述符中分离出与分页、信号量、文件系统、打开文件描述符、命名空间以及I/O权限位图相关的数据结构。如果没有其它进程共享这些数据结构,那么这些函数还删除所有这些数据结构中。

  4. 如果实现了被杀死进程的执行域和可执行格式的内核函数包含在内核模块中,则函数递减它们的使用计数器。

  5. 把进程描述符的exit_code字段设置成进程的终止代号,这个值要么是_exit()或exit_group()系统调用参数,要么是由内核提供的一个错误代码。

  6. 调用exit_notify()函数执行下面的操作:

    • 更新父进程和子进程的亲属关系。如果同一线程组中有正在运行的进程,就让终止进程所创建的所有子进程都变成同一线程组中另外一个进程的子进程,否则让它们成为init的子进程

    • 检查被终止进程其进程描述符的exit_signal字段是否不等于-1,并检查进程是否是其所属进程组的最后一个成员。在这种情况下,函数通过给正被终止进程的父进程发送一个信号,以通知父进程子进程死亡。

    • 否则,也就是exit_signal字段等于-1,或者线程组中还有其它进程,那么只要进程正在被跟踪,就向父进程发送一个SIGCHLD信号。

    • 如果进程描述符的exit_signal字段等于-1,而且进程没有被跟踪,就把进程描述符的exit_state字段置为EXIT_DEAD,然后调用release_task()回收进程的其它数据结构占用的内存,并递减进程描述符的使用计数器,以使进程描述符本身正好不会被释放。

    • 否则,如果进程描述符的exit_signal字段不等于-1,或进程正在被跟踪,就把exit_state字段置为EXIT_ZOMBIE

    • 把进程描述符的flags字段设置为PF_DEAD标志。

  1. 调用schedule()函数选择一个新进程运行。调度程序忽略处于EXIT_ZOMBIE状态的进程,所以这种进程正好在schedule()中的宏switch_to被调用之后停止执行。

Reference

Unix/Linux编程:进程资源限制_如何限制一个进程能够使用的线程等资源-CSDN博客

Linux进程管理(2):进程创建的copy_process和进程销毁_copy process-CSDN博客

相关推荐
keep__go1 小时前
Linux 批量配置互信
linux·运维·服务器·数据库·shell
矛取矛求1 小时前
Linux中给普通账户一次性提权
linux·运维·服务器
Fanstay9851 小时前
在Linux中使用Nginx和Docker进行项目部署
linux·nginx·docker
大熊程序猿1 小时前
ubuntu 安装kafka-eagle
linux·ubuntu·kafka
daizikui3 小时前
Linux文件目录命令
linux·运维·服务器
NikitaC3 小时前
ldconfig 和 LD_LIBRARY_PATH 区别
linux·c++
清源妙木真菌3 小时前
Linux:进程概念
linux
许嵩664 小时前
IC 脚本之VIM 记录
linux·编辑器·vim
花生的酱5 小时前
Shell编程之正则表达式与文本处理器
linux·运维·正则表达式