Linux内核深度解析之进程管理丨内含赠书福利(一)
2.1 进程
Linux内核把进程称为任务(task),进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。
进程有两种特殊形式:没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程,通常在不会引起混淆的情况下把用户线程简称为线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。
C标准库的进程术语和Linux内核的进程术语的对应关系如表2.1所示。
表2.1 进程术语的对应关系
C标准库的进程术语 | 对应的Linux内核的进程术语 |
---|---|
包含多个线程的进程 | 线程组 |
只有一个线程的进程 | 进程或任务 |
线程 | 共享用户虚拟地址空间的进程 |
结构体task_struct是进程描述符,其主要成员如表2.2所示。
表2.2 进程描述符task_struct的主要成员
成员 | 说明 |
---|---|
volatile long state; | 进程的状态 |
void *stack; | 指向内核栈 |
pid_t pid; | 全局的进程号 |
pid_t tgid; | 全局的线程组标识符 |
struct pid_link pids[PIDTYPE_MAX]; | 进程号,进程组标识符和会话标识符 |
struct task_struct __rcu *real_parent; struct task_struct __rcu *parent; | real_parent指向真实的父进程 parent指向父进程:如果进程被另一个进程(通常是调试器)使用系统调用ptrace跟踪,那么父进程是跟踪进程,否则和real_parent相同 |
struct task_struct *group_leader; | 指向线程组的组长 |
const struct cred __rcu *real_cred; const struct cred __rcu *cred; | real_cred指向主体和真实客体证书,cred指向有效客体证书。通常情况下,cred和real_cred指向相同的证书,但是cred可以被临时改变 |
char comm[TASK_COMM_LEN]; | 进程名称 |
int prio, static_prio, normal_prio; unsigned int rt_priority; unsigned int policy; | 调度策略和优先级 |
cpumask_t cpus_allowed | 允许进程在哪些处理器上运行 |
struct mm_struct *mm,*active_mm; | 指向内存描述符 进程:mm和active_mm指向同一个内存描述符 内核线程:mm是空指针,当内核线程运行时,active_mm指向从进程借用的内存描述符 |
struct fs_struct *fs; | 文件系统信息,主要是进程的根目录和当前工作目录 |
struct files_struct *files; | 打开文件表 |
struct nsproxy *nsproxy; | 命名空间 |
struct signal_struct *signal; struct sighand_struct *sighand; sigset_t blocked, real_blocked; sigset_t saved_sigmask; struct sigpending pending; | 信号处理 (结构体signal_struct比较混乱,里面包含很多和信号无关的成员,没有人愿意整理) |
struct sysv_sem sysvsem; struct sysv_shm sysvshm; | UNIX系统5信号量和共享内存 |
2.2 命名空间
和虚拟机相比,容器是一种轻量级的虚拟化技术,直接使用宿主机的内核,使用命名空间隔离资源。Linux内核提供的命名空间如表2.3所示。
表2.3 命名空间
命名空间 | 隔离资源 |
---|---|
控制组(cgroup) | 控制组根目录 |
进程间通信(IPC) | UNIX系统5进程间通信和POSIX消息队列 |
网络(network) | 网络协议栈 |
挂载(mount) | 挂载点 |
进程号(PID) | 进程号 |
用户(user) | 用户标识符和组标识符 |
UNIX分时系统(UNIX Timesharing System,UTS) | 主机名和网络信息服务(NIS)域名 |
可以使用以下两种方法创建新的命名空间。
(1)调用clone创建子进程时,使用标志位控制子进程是共享父进程的命名空间还是创建新的命名空间。
(2)调用unshare创建新的命名空间,不和已存在的任何其他进程共享命名空间。
进程也可以使用系统调用setns,绑定到一个已经存在的命名空间。
如图2.1所示,进程描述符的成员“nsproxy”指向一个命名空间代理,命名空间代理包含除了用户以外的所有其他命名空间的地址。如果父进程和子进程共享除了用户以外的所有其他命名空间,那么它们共享一个命名空间代理。
图2.1 进程的命名空间
本节只介绍进程号命名空间。
进程号命名空间用来隔离进程号,对应的结构体是pid_namespace。每个进程号命名空间独立分配进程号。进程号命名空间按层次组织成一棵树,初始进程号命名空间是树的根,对应全局变量init_pid_ns,所有进程默认属于初始进程号命名空间。
创建进程时,从进程所属的进程号命名空间到初始进程号命名空间都会分配进程号。如图2.2所示,假设某个进程属于进程号命名空间b,b的父命名空间是a,a的父命名空间是初始进程号命名空间,从b到初始的每一级命名空间依次分配进程号10、20和30。
图2.2 进程号命名空间
2.3 进程标识符
进程有以下标识符。
(1)进程标识符:进程所属的进程号命名空间到根的每层命名空间,都会给进程分配一个标识符。
(2)线程组标识符:多个共享用户虚拟地址空间的进程组成一个线程组,线程组中的主进程称为组长,线程组标识符就是组长的进程标识符。当调用系统调用clone传入标志CLONE_THREAD以创建新进程时,新进程和当前进程属于一个线程组。
进程描述符的成员tgid存放线程组标识符,成员group_leader指向组长的进程描述符。
(3)进程组标识符:多个进程可以组成一个进程组,进程组标识符是组长的进程标识符。进程可以使用系统调用setpgid创建或者加入一个进程组。会话和进程组被设计用来支持shell作业控制,shell为执行单一命令或者管道的进程创建一个进程组。进程组简化了向进程组的所有成员发送信号的操作。
(4)会话标识符:多个进程组可以组成一个会话。当进程调用系统调用setsid的时候,创建一个新的会话,会话标识符是该进程的进程标识符。创建会话的进程是会话的首进程。
Linux是多用户操作系统,用户登录时会创建一个会话,用户启动的所有进程都属于这个会话。登录shell是会话首进程,它所使用的终端就是会话的控制终端,会话首进程通常也被称为控制进程。当用户退出登录时,所有属于这个会话的进程都将被终止。
假设某个进程属于进程号命名空间b,b的父命名空间是a,a的父命名空间是初始进程号命名空间,从b到初始的每一级命名空间分配的进程号依次是10、20和30。进程标识符数据结构如图2.3所示,进程描述符的相关成员如下。
(1)成员pid存储全局进程号,即初始进程号命名空间分配的进程号30。
(2)成员pids[PIDTYPE_PID].pid指向结构体pid,存放3个命名空间分配的进程号。
(3)成员pids[PIDTYPE_PGID].pid指向进程组组长的结构体pid(限于篇幅,图2.3中没画出)。
(4)成员pids[PIDTYPE_SID].pid指向会话首进程的结构体pid(限于篇幅,图2.3中没画出)。
进程标识符结构体pid的成员如下。
(1)成员count是引用计数。
(2)成员level是进程所属的进程号命名空间的层次。
(3)数组numbers的元素个数是成员level的值加上1,3个元素依次存放初始命名空间、a和b三个命名空间分配的进程号。numbers[i].nr是进程号命名空间分配的进程号,numbers[i].ns指向进程号命名空间的结构体pid_namespace,numbers[i].pid_chain用来把进程加入进程号散列表pid_hash,根据进程号和命名空间计算散列值。
图2.3 进程标识符数据结构
2.4 进程关系
进程1分叉生成进程2,进程1称为父进程,进程2称为子进程。
进程1多次分叉生成进程2和进程3,进程2和进程3的关系是兄弟关系。
如图2.4所示,一个进程的所有子进程被链接在一条子进程链表上,头节点是父进程的成员children,链表节点是子进程的成员sibling。子进程的成员real_parent指向父进程的进程描述符,成员parent用来干什么呢?如果子进程被某个进程(通常是调试器)使用系统调用ptrace跟踪,那么成员parent指向跟踪者的进程描述符,否则成员parent也指向父进程的进程描述符。
如图2.5所示,进程管理子系统把所有进程链接在一条进程链表上,头节点是0号线程的成员tasks,链表节点是每个进程的成员tasks。对于线程组,只把组长加入进程链表。
图2.4 父子进程
图2.5 进程和线程链表
一个线程组的所有线程链接在一条线程链表上,头节点是组长的成员thread_group,链表节点是线程的成员thread_group。线程的成员group_leader指向组长的进程描述符,成员tgid是线程组标识符,成员pid存放自己的进程标识符。
2.5 启动程序
当我们在shell进程里面执行命令“/sbin/hello.elf &”以启动程序“hello”时,shell进程首先创建子进程,然后子进程装载程序“hello.elf”,其代码如下:
下面描述创建新进程和装载程序的过程。
2.5.1 创建新进程
在Linux内核中,新进程是从一个已经存在的进程复制出来的。内核使用静态数据构造出0号内核线程,0号内核线程分叉生成1号内核线程和2号内核线程(kthreadd线程)。1号内核线程完成初始化以后装载用户程序,变成1号进程,其他进程都是1号进程或者它的子孙进程分叉生成的;其他内核线程是kthreadd线程分叉生成的。
3个系统调用可以用来创建新的进程。
(1)fork(分叉):子进程是父进程的一个副本,采用了写时复制的技术。
(2)vfork:用于创建子进程,之后子进程立即调用execve以装载新程序的情况。为了避免复制物理页,父进程会睡眠等待子进程装载新程序。现在fork采用了写时复制的技术,vfork失去了速度优势,已经被废弃。
(3)clone(克隆):可以精确地控制子进程和父进程共享哪些资源。这个系统调用的主要用处是可供pthread库用来创建线程。
clone是功能最齐全的函数,参数多,使用复杂,fork是clone的简化函数。
我们先介绍Linux内核定义系统调用的独特方式,以系统调用fork为例:
把宏展开以后是:
“SYSCALL_DEFINE”后面的数字表示系统调用的参数个数,“SYSCALL_DEFINE0”表示系统调用没有参数,“SYSCALL_DEFINE6”表示系统调用有6个参数,如果参数超过6个,使用宏“SYSCALL_DEFINEx”。
“asmlinkage”表示这个C语言函数可以被汇编代码调用。如果使用C++编译器,“asmlinkage”被定义为extern "C";如果使用C编译器,“asmlinkage”是空的宏。
系统调用的函数名称以“sys_”开头。
创建新进程的进程p和生成的新进程的关系有3种情况。
(1)新进程是进程p的子进程。
(2)如果clone传入标志位CLONE_PARENT,那么新进程和进程p拥有同一个父进程,是兄弟关系。
(3)如果clone传入标志位CLONE_THREAD,那么新进程和进程p属于同一个线程组。
创建新进程的3个系统调用在文件“kernel/fork.c”中,它们把工作委托给函数_do_fork。
1.函数_do_fork
函数_do_fork的原型如下:
参数如下。
(1)参数clone_flags是克隆标志,最低字节指定了进程退出时发给父进程的信号,创建线程时,该参数的最低字节是0,表示线程退出时不需要向父进程发送信号。
(2)参数stack_start只在创建线程时有意义,用来指定新线程的用户栈的起始地址。
(3)参数stack_size只在创建线程时有意义,用来指定新线程的用户栈的长度。这个参数已经废弃。
(4)参数parent_tidptr只在创建线程时有意义,如果参数clone_flags指定了标志位CLONE_PARENT_SETTID,那么调用线程需要把新线程的进程标识符写到参数parent_tidptr指定的位置,也就是新线程保存自己的进程标识符的位置。
(5)参数child_tidptr只在创建线程时有意义,存放新线程保存自己的进程标识符的位置。如果参数clone_flags指定了标志位CLONE_CHILD_CLEARTID,那么线程退出时需要清除自己的进程标识符。如果参数clone_flags指定了标志位CLONE_CHILD_SETTID,那么新线程第一次被调度时需要把自己的进程标识符写到参数child_tidptr指定的位置。
(6)参数tls只在创建线程时有意义,如果参数clone_flags指定了标志位CLONE_SETTLS,那么参数tls指定新线程的线程本地存储的地址。
如图2.6所示,函数_do_fork的执行流程如下。
图2.6 函数_do_fork的执行流程
(1)调用函数copy_process以创建新进程。
(2)如果参数clone_flags设置了标志CLONE_PARENT_SETTID,那么把新线程的进程标识符写到参数parent_tidptr指定的位置。
(3)调用函数wake_up_new_task以唤醒新进程。
(4)如果是系统调用vfork,那么当前进程等待子进程装载程序。
2.函数copy_process
创建新进程的主要工作由函数copy_process实现,其执行流程如图2.7所示。
图2.7 函数copy_process的执行流程
(1)检查标志:以下标志组合是非法的。
1)同时设置CLONE_NEWNS和CLONE_FS,即新进程属于新的挂载命名空间,同时和当前进程共享文件系统信息。
2)同时设置CLONE_NEWUSER和CLONE_FS,即新进程属于新的用户命名空间,同时和当前进程共享文件系统信息。
3)设置CLONE_THREAD,未设置CLONE_SIGHAND,即新进程和当前进程属于同一个线程组,但是不共享信号处理程序。
4)设置CLONE_SIGHAND,未设置CLONE_VM,即新进程和当前进程共享信号处理程序,但是不共享虚拟内存。
5)新进程想要和当前进程成为兄弟进程,并且当前进程是某个进程号命名空间中的1号进程。这种标志组合是非法的,说明1号进程不存在兄弟进程。
6)新进程和当前进程属于同一个线程组,同时新进程属于不同的用户命名空间或者进程号命名空间。这种标志组合是非法的,说明同一个线程组的所有线程必须属于相同的用户命名空间和进程号命名空间。
(2)函数dup_task_struct:函数dup_task_struct为新进程的进程描述符分配内存,把当前进程的进程描述符复制一份,为新进程分配内核栈。
如图2.8所示,进程描述符的成员stack指向内核栈。
图2.8 进程的内核栈
内核栈的定义如下:
内核栈有两种布局。
1)结构体thread_info占用内核栈的空间,在内核栈顶部,成员task指向进程描述符。
2)结构体thread_info没有占用内核栈的空间,是进程描述符的第一个成员。
两种布局的区别是结构体thread_info的位置不同。如果选择第二种布局,需要打开配置宏CONFIG_THREAD_INFO_IN_TASK。ARM64架构使用第二种内核栈布局。第二种内核栈布局的好处是:thread_info结构体作为进程描述符的第一个成员,它的地址和进程描述符的地址相同。当进程在内核模式运行时,ARM64架构的内核使用用户栈指针寄存器SP_EL0存放当前进程的thread_info结构体的地址,通过这个寄存器既可以得到thread_info结构体的地址,也可以得到进程描述符的地址。
内核栈的长度是THREAD_SIZE,它由各种处理器架构自己定义,ARM64架构定义的内核栈长度是16KB。
结构体thread_info存放汇编代码需要直接访问的底层数据,由各种处理器架构定义,ARM64架构定义的结构体如下。
1)flags:底层标志,常用的标志是_TIF_SIGPENDING和_TIF_NEED_RESCHED,前者表示进程有需要处理的信号,后者表示调度器需要重新调度进程。
2)addr_limit:进程可以访问的地址空间的上限。对于进程,它的值是用户地址空间的上限;对于内核线程,它的值是内核地址空间的上限。
3)preempt_count:抢占计数器。
(3)检查用户的进程数量限制:如果拥有当前进程的用户创建的进程数量达到或者超过限制,并且用户不是根用户,也没有忽略资源限制的权限(CAP_SYS_RESOURCE)和系统管理权限(CAP_SYS_ADMIN),那么不允许创建新进程。
(4)函数copy_creds:函数copy_creds负责复制或共享证书,证书存放进程的用户标识符、组标识符和访问权限。
如果设置了标志CLONE_THREAD,即新进程和当前进程属于同一个线程组,那么新进程和当前进程共享证书,如图2.9所示。
图2.9 线程共享证书
否则,子进程复制当前进程的证书,如果设置了标志CLONE_NEWUSER,那么需要为新进程创建新的用户命名空间,新的用户命名空间是当前进程的用户命名空间的子命名空间。
最后把用户的进程数量统计值加1。
(5)检查线程数量限制:如果线程数量达到允许的线程最大数量,那么不允许创建新进程。
全局变量nr_threads 存放当前的线程数量;max_threads存放允许创建的线程最大数量,默认值是MAX_THREADS。
(6)函数sched_fork:函数sched_fork为新进程设置调度器相关的参数,其主要代码如下。
第6行代码,调用函数__sched_fork以执行基本设置。
第7行代码,把新进程的状态设置为TASK_NEW。
第9行代码,把新进程的调度优先级设置为当前进程的正常优先级。为什么不设置为当前进程的调度优先级?因为当前进程可能因为占有实时互斥锁而被临时提升了优先级。
第11~23行代码,如果当前进程使用sched_setscheduler设置调度策略和相关参数时设置了标志SCHED_RESET_ON_FORK,要求创建新进程时把新进程的调度策略和优先级设置为默认值,那么处理如下。
第12~15行代码,如果当前进程是限期进程或实时进程,那么把新进程的调度策略恢复成SCHED_NORMAL,把nice值设置成默认值0,对应静态优先级120。
第16行和第17行代码,如果当前进程是普通进程,并且nice值小于0,那么把新进程的nice值恢复成默认值0,对应静态优先级120。
第25~32行代码,根据新进程的调度优先级设置调度类。
第25~27行代码,如果调度优先级是限期调度类的优先级,那么返回“-EAGAIN”,因为不允许限期进程分叉生成新的限期进程。
第28行和第29行代码,如果调度优先级是实时调度类的优先级,那么把调度类设置为实时调度类。
第30行和第31行代码,如果调度优先级是公平调度类的优先级,那么把调度类设置为公平调度类。
第37行代码,调用函数__set_task_cpu,设置新进程在哪个处理器上,如果开启公平组调度和实时组调度,那么还需要设置新进程属于哪个公平运行队列和哪个实时运行队列。
第38行和第39行代码,执行调度类的task_fork方法。
第46行代码,初始化新进程的抢占计数器,在抢占式内核中设置为2,在非抢占式内核中设置为0。因为在抢占式内核中,如果函数schedule()在调度进程时选中了新进程,那么调用函数rq_unlock_irq()和sched_preempt_enable_no_resched()时会把新进程的抢占计数减两次。
(7)复制或者共享资源如下。
1)UNIX系统5信号量。只有属于同一个线程组的线程之间才会共享UNIX系统5信号量。函数copy_semundo处理UNIX系统5信号量的共享问题,其代码如下:
第6~11行代码,如果调用者传入标志CLONE_SYSVSEM,表示共享UNIX系统5信号量,那么新进程和当前进程共享UNIX系统5信号量的撤销请求链表,对应结构体sem_undo_list,把计数加1。当进程退出时,内核需要把信号量的计数值加上该进程曾经减去的数值。
否则,在第12行和第13行代码中,新进程的UNIX系统5信号量的撤销请求链表是空的。
2)打开文件表。只有属于同一个线程组的线程之间才会共享打开文件表。函数copy_files复制或者共享打开文件表,其代码如下:
第10~13行代码,如果调用者传入标志CLONE_FILES,表示共享打开文件表,那么新进程和当前进程共享打开文件表的结构体files_struct,把计数加1。
否则,在第15行代码中,新进程把当前进程的打开文件表复制一份。
3)文件系统信息。进程的文件系统信息包括根目录、当前工作目录和文件模式创建掩码。只有属于同一个线程组的线程之间才会共享文件系统信息。
函数copy_fs复制或者共享文件系统信息,其代码如下:
第4~13行代码,如果调用者传入标志CLONE_FS,表示共享文件系统信息,那么新进程和当前进程共享文件系统信息的结构体fs_struct,把计数users加1。
否则,在第14行代码中,新进程把当前进程的文件系统信息复制一份。
4)信号处理程序。只有属于同一个线程组的线程之间才会共享信号处理程序。函数copy_sighand复制或者共享信号处理程序,其代码如下:
第5~8行代码,如果调用者传入标志CLONE_SIGHAND,表示共享信号处理程序,那么新进程和当前进程共享信号处理程序的结构体sighand_struct,把计数加1。
否则,在第9~15行代码中,新进程把当前进程的信号处理程序复制一份。
5)信号结构体。只有属于同一个线程组的线程之间才会共享信号结构体。函数copy_signal复制或共享信号结构体,其代码如下:
第5行代码,如果调用者传入标志CLONE_THREAD,表示创建线程,那么新进程和当前进程共享信号结构体signal_struct。
否则,在第8~20行代码中,为新进程分配信号结构体,然后初始化,继承当前进程的资源限制。
6)虚拟内存。只有属于同一个线程组的线程之间才会共享虚拟内存。函数copy_mm复制或共享虚拟内存,其主要代码如下:
第15~19行代码,如果调用者传入标志CLONE_VM,表示共享虚拟内存,那么新进程和当前进程共享内存描述符mm_struct,把计数mm_users加1。
否则,在第22~28行代码中,新进程复制当前进程的虚拟内存。
7)命名空间。函数copy_namespaces创建或共享命名空间,其代码如下:
第7~12行代码,如果共享除了用户以外的所有其他命名空间,那么新进程和当前进程共享命名空间代理结构体nsproxy,把计数加1。
第14行和第15行代码,如果进程没有系统管理权限,那么不允许创建新的命名空间。
第17~19行代码,如果既要求创建新的进程间通信命名空间,又要求共享UNIX系统5信号量,那么这种要求是不合理的。
第21行代码,创建新的命名空间代理,然后创建或者共享命名空间。
如果设置了标志CLONE_NEWNS,那么创建新的挂载命名空间,否则共享挂载命名空间。
如果设置了标志CLONE_NEWUTS,那么创建新的UTS命名空间,否则共享UTS命名空间。
如果设置了标志CLONE_NEWIPC,那么创建新的进程间通信命名空间,否则共享进程间通信命名空间。
如果设置了标志CLONE_NEWPID,那么创建新的进程号命名空间,否则共享进程号命名空间。
如果设置了标志CLONE_NEWCGROUP,那么创建新的控制组命名空间,否则共享控制组命名空间。
如果设置了标志CLONE_NEWNET,那么创建新的网络命名空间,否则共享网络命名空间。
8)I/O上下文。函数copy_io创建或者共享I/O上下文,其代码如下:
第10~12行代码,如果调用者传入标志CLONE_IO,表示共享I/O上下文,那么共享I/O上下文的结构体io_context,把计数nr_tasks加1。
否则,在第13~20行代码中,创建新的I/O上下文,然后初始化,继承当前进程的I/O优先级。
9)复制寄存器值。调用函数copy_thread_tls复制当前进程的寄存器值,并且修改一部分寄存器值。如图2.10所示,进程有两处用来保存寄存器值:从用户模式切换到内核模式时,把用户模式的各种寄存器保存在内核栈底部的结构体pt_regs中;进程调度器调度进程时,切换出去的进程把寄存器值保存在进程描述符的成员thread中。因为不同处理器架构的寄存器不同,所以各种处理器架构需要自己定义结构体pt_regs和thread_struct,实现函数copy_thread_tls。
图2.10 进程保存寄存器值处
ARM64架构的函数copy_thread_tls把主要工作委托给函数copy_thread,函数copy_thread的代码如下:
执行过程如下。
第6行代码,把新进程的进程描述符的成员thread.cpu_context清零,在调度进程时切换出去的进程使用这个成员保存通用寄存器的值。
第8~30行代码,如果是用户进程,其处理过程如下。
第9行代码,子进程把当前进程内核栈底部的pt_regs结构体复制一份。当前进程从用户模式切换到内核模式时,把用户模式的各种寄存器保存一份放在内核栈底部的pt_regs结构体中。
第10行代码,把子进程的X0寄存器设置为0,因为X0寄存器存放系统调用的返回值,调用fork或clone后,子进程返回0。
第16行代码,把子进程的TPIDR_EL0寄存器设置为当前进程的TPIDR_EL0寄存器的值。TPIDR_EL0是用户读写线程标识符寄存器(User Read and Write Thread ID Register),pthread库用来存放每线程数据的基准地址,存放每线程数据的区域通常被称为线程本地存储(Thread Local Storage,TLS)。
第18~23行代码,如果使用系统调用clone创建线程时指定了用户栈的起始地址,那么把新线程的栈指针寄存器SP_EL0设置为用户栈的起始地址。
第29行和第30行代码,如果使用系统调用clone创建线程时设置了标志位CLONE_SETTLS,那么把新线程的TPIDR_EL0寄存器设置为系统调用clone第4个参数tls指定的线程本地存储的地址。
第31~39行代码,如果是内核线程,其处理过程如下。
第32行代码,把子进程内核栈底部的pt_regs结构体清零。
第33行代码,把子进程的处理器状态设置为PSR_MODE_EL1h,值为5,其中第0位是栈指针选择符(ARM64架构在异常级别1时可以使用异常级别1的栈指针寄存器SP_EL1,也可以使用异常级别0的栈指针寄存器SP_EL0),值为1表示选择栈指针寄存器SP_EL1;第2、3位是异常级别,值为1表示异常级别1。
第37行代码,把子进程的x19寄存器设置为线程函数的地址,注意参数stack_start存放线程函数的地址,即用来创建内核线程的函数kernel_thread的第一个参数fn。
第38行代码,把子进程的x20寄存器设置为传给线程函数的参数,注意参数stk_sz存放传给线程函数的参数,即用来创建内核线程的函数kernel_thread的第二个参数arg。
第40行代码,把子进程的程序计数器设置为函数ret_from_fork。当子进程被调度时,从函数ret_from_fork开始执行。
第41行代码,把子进程的栈指针寄存器SP_EL1设置为内核栈底部pt_regs结构体的起始位置。
(8)设置进程号和进程关系。函数copy_process的最后部分为新进程设置进程号和进程关系,其主要代码如下:
第1~7行代码,为新进程分配进程号。从新进程所属的进程号命名空间一直到根,每层进程号命名空间为新进程分配一个进程号。
pid等于init_struct_pid的地址,这是什么意思呢?在内核初始化时,引导处理器为每个从处理器分叉生成一个空闲线程(参考函数idle_threads_init),所有处理器的空闲线程使用进程号0,全局变量init_struct_pid存放空闲线程的进程号。
第12~23行代码,分情况设置新进程退出时发送给父进程的信号,设置新进程所属的线程组。
1)第12~15行代码,如果是创建线程,那么把新线程的成员exit_signal设置为−1,新线程退出时不需要发送信号给父进程;因为新线程和当前线程属于同一个线程组,所以成员group_leader指向同一个组长,成员tgid存放组长的进程号。
2)第16~23行代码,如果是创建进程,执行过程如下。
第17行和第18行代码,如果传入标志CLONE_PARENT,新进程和当前进程是兄弟关系,那么新进程的成员exit_signal等于当前进程所属线程组的组长的成员exit_signal。
第19行和第20行代码,如果没有传入标志CLONE_PARENT,新进程和当前进程是父子关系,那么新进程的成员exit_signal是调用者指定的信号。
第21行和第22行代码,新进程所属线程组的组长是自己。
第27~29行代码,控制组的进程数控制器检查是否允许创建新进程:从当前进程所属的控制组一直到控制组层级的根,如果其中一个控制组的进程数量大于或等于限制,那么不允许使用fork和clone创建新进程。
控制组(cgroup)的进程数(PIDs)控制器:用来限制控制组及其子控制组中的进程使用fork和clone创建的新进程的数量,如果进程p所属的控制组到控制组层级的根,其中有一个控制组的进程数量大于或等于限制,那么不允许进程p使用fork和clone创建新进程。
第33~39行代码,为新进程设置父进程。
第34行代码,如果传入了标志CLONE_PARENT(表示拥有相同的父进程)或者CLONE_THREAD(表示创建线程),那么新进程和当前进程拥有相同的父进程。
第37行代码,否则,新进程的父进程是当前进程。
第46行代码,把新进程的成员pids[PIDTYPE_PID].pid指向第2行代码分配的进程号结构体。
第47~64行代码,如果是创建新进程,执行下面的处理过程。
第48行代码,因为新进程和当前进程属于同一个进程组,所以成员pids[PIDTYPE_PGID].pid指向同一个进程组的组长的进程号结构体。
第49行代码,因为新进程和当前进程属于同一个会话,所以成员pids[PIDTYPE_SID].pid指向同一个会话的控制进程的进程号结构体。
第51~53行代码,如果新进程是1号进程,那么新进程是进程号命名空间中的孤儿进程领养者,忽略致命的信号,因为1号进程是不能杀死的。如果把1号进程杀死了,谁来领养孤儿进程呢?
第60行代码,把新进程添加到父进程的子进程链表中。
第61行代码,把新进程添加到进程链表中,链表节点是成员tasks,头节点是空闲线程的成员tasks(init_task.tasks)。
第62行代码,把新进程添加到进程组的进程链表中。
第63行代码,把新进程添加到会话的进程链表中。
第65~73行代码,如果是创建线程,执行下面的处理过程。
第66行代码,把线程组的线程计数值加1。
第67行代码,把线程组的第2个线程计数值加1,这个计数值是原子变量。
第68行代码,把信号结构体的引用计数加1。
第69行和第70行代码,把线程加入线程组的线程链表中,链表节点是成员thread_group,头节点是组长的成员thread_group。
第71行和第72行代码,把线程加入线程组的第二条线程链表中,链表节点是成员thread_node,头节点是信号结构体的成员thread_head。
第74行代码,把新进程添加到进程号结构体的进程链表中。
第75行代码,把线程计数值加1。
第83行代码,调用函数proc_fork_connector,通过进程事件连接器向用户空间通告进程事件PROC_EVENT_FORK。进程可以通过进程事件连接器监视进程事件:创建协议号为NETLINK_CONNECTOR的netlink套接字,然后绑定到多播组CN_IDX_PROC。
本文转载自异步社区
原文链接:https://www.epubit.com/articleDetails?id=Nf4557a1e-22ab-414f-b892-c8feb0fe27de
- 点赞
- 收藏
- 关注作者
评论(0)