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_DEFINE0(fork)
把宏展开以后是:
asmlinkage long sys_fork(void)
“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的原型如下:
long _do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr, unsigned long tls);
参数如下。
(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 进程的内核栈
内核栈的定义如下:
<include/linux/sched.h>
union thread_union {
#ifndef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info;
#endif
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
内核栈有两种布局。
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架构定义的结构体如下。
<arch/arm64/include/asm/thread_info.h> struct thread_info { unsigned long flags; /*底层标志位*/ mm_segment_t addr_limit; /*地址限制 */ #ifdef CONFIG_ARM64_SW_TTBR0_PAN u64 ttbr0; /* 保存的寄存器 TTBR0_EL1 */ #endif int preempt_count; /* 0表示可抢占,小于0是缺陷 */ };
1)flags:底层标志,常用的标志是_TIF_SIGPENDING和_TIF_NEED_RESCHED,前者表示进程有需要处理的信号,后者表示调度器需要重新调度进程。
2)addr_limit:进程可以访问的地址空间的上限。对于进程,它的值是用户地址空间的上限;对于内核线程,它的值是内核地址空间的上限。
3)preempt_count:抢占计数器。
(3)检查用户的进程数量限制:如果拥有当前进程的用户创建的进程数量达到或者超过限制,并且用户不是根用户,也没有忽略资源限制的权限(CAP_SYS_RESOURCE)和系统管理权限(CAP_SYS_ADMIN),那么不允许创建新进程。
if (atomic_read(&p->real_cred->user->processes) >= task_rlimit(p, RLIMIT_NPROC)) { if (p->real_cred->user ! = INIT_USER && !capable(CAP_SYS_RESOURCE) && ! capable(CAP_SYS_ADMIN)) goto bad_fork_free; }
(4)函数copy_creds:函数copy_creds负责复制或共享证书,证书存放进程的用户标识符、组标识符和访问权限。
如果设置了标志CLONE_THREAD,即新进程和当前进程属于同一个线程组,那么新进程和当前进程共享证书,如图2.9所示。
图2.9 线程共享证书
否则,子进程复制当前进程的证书,如果设置了标志CLONE_NEWUSER,那么需要为新进程创建新的用户命名空间,新的用户命名空间是当前进程的用户命名空间的子命名空间。
最后把用户的进程数量统计值加1。
(5)检查线程数量限制:如果线程数量达到允许的线程最大数量,那么不允许创建新进程。
全局变量nr_threads存放当前的线程数量;max_threads存放允许创建的线程最大数量,默认值是MAX_THREADS。
if (nr_threads >= max_threads) goto bad_fork_cleanup_count;
(6)函数sched_fork:函数sched_fork为新进程设置调度器相关的参数,其主要代码如下。
kernel/sched/core.c
1 int sched_fork(unsigned long clone_flags, struct task_struct *p)
2 {
3 unsigned long flags;
4 int cpu = get_cpu();
5
6 __sched_fork(clone_flags, p);
7 p->state = TASK_NEW;
8
9 p->prio = current->normal_prio;
10
11 if (unlikely(p->sched_reset_on_fork)) {
12 if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
13 p->policy = SCHED_NORMAL;
14 p->static_prio = NICE_TO_PRIO(0);
15 p->rt_priority = 0;
16 } else if (PRIO_TO_NICE(p->static_prio) < 0)
17 p->static_prio = NICE_TO_PRIO(0);
18
19 p->prio = p->normal_prio = __normal_prio(p);
20 set_load_weight(p);
21
22 p->sched_reset_on_fork = 0;
23 }
24
25 if (dl_prio(p->prio)) {
26 put_cpu();
27 return -EAGAIN;
28 } else if (rt_prio(p->prio)) {
29 p->sched_class = &rt_sched_class;
30 } else {
31 p->sched_class = &fair_sched_class;
32 }
33
34 init_entity_runnable_average(&p->se);
35
36 raw_spin_lock_irqsave(&p->pi_lock, flags);
37 __set_task_cpu(p, cpu);
38 if (p->sched_class->task_fork)
39 p->sched_class->task_fork(p);
40 raw_spin_unlock_irqrestore(&p->pi_lock, flags);
41
42 …
43 #if defined(CONFIG_SMP)
44 p->on_cpu = 0;
45 #endif
46 init_task_preempt_count(p);
47 …
48
49 put_cpu();
50 return 0;
51 }
第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信号量的共享问题,其代码如下:
kernel/fork.c
1 int copy_semundo(unsigned long clone_flags, struct task_struct *tsk)
2 {
3 struct sem_undo_list *undo_list;
4 int error;
5
6 if (clone_flags & CLONE_SYSVSEM) {
7 error = get_undo_list(&undo_list);
8 if (error)
9 return error;
10 atomic_inc(&undo_list->refcnt);
11 tsk->sysvsem.undo_list = undo_list;
12 } else
13 tsk->sysvsem.undo_list = NULL;
14
15 return 0;
16 }
第6~11行代码,如果调用者传入标志CLONE_SYSVSEM,表示共享UNIX系统5信号量,那么新进程和当前进程共享UNIX系统5信号量的撤销请求链表,对应结构体sem_undo_list,把计数加1。当进程退出时,内核需要把信号量的计数值加上该进程曾经减去的数值。
否则,在第12行和第13行代码中,新进程的UNIX系统5信号量的撤销请求链表是空的。
2)打开文件表。只有属于同一个线程组的线程之间才会共享打开文件表。函数copy_files复制或者共享打开文件表,其代码如下:
kernel/fork.c 1 static int copy_files(unsigned long clone_flags, struct task_struct *tsk) 2 { 3 struct files_struct *oldf, *newf; 4 int error = 0; 5 6 oldf = current->files; 7 if (! oldf) /* 后台进程可能没有打开文件表 */ 8 goto out; 9 10 if (clone_flags & CLONE_FILES) { 11 atomic_inc(&oldf->count); 12 goto out; 13 } 14 15 newf = dup_fd(oldf, &error); 16 if (! newf) 17 goto out; 18 19 tsk->files = newf; 20 error = 0; 21 out: 22 return error; 23 }
第10~13行代码,如果调用者传入标志CLONE_FILES,表示共享打开文件表,那么新进程和当前进程共享打开文件表的结构体files_struct,把计数加1。
否则,在第15行代码中,新进程把当前进程的打开文件表复制一份。
3)文件系统信息。进程的文件系统信息包括根目录、当前工作目录和文件模式创建掩码。只有属于同一个线程组的线程之间才会共享文件系统信息。
函数copy_fs复制或者共享文件系统信息,其代码如下:
kernel/fork.c
1 static int copy_fs(unsigned long clone_flags, struct task_struct *tsk)
2 {
3 struct fs_struct *fs = current->fs;
4 if (clone_flags & CLONE_FS) {
5 spin_lock(&fs->lock);
6 if (fs->in_exec) {
7 spin_unlock(&fs->lock);
8 return -EAGAIN;
9 }
10 fs->users++;
11 spin_unlock(&fs->lock);
12 return 0;
13 }
14 tsk->fs = copy_fs_struct(fs);
15 if (! tsk->fs)
16 return -ENOMEM;
17 return 0;
18 }
第4~13行代码,如果调用者传入标志CLONE_FS,表示共享文件系统信息,那么新进程和当前进程共享文件系统信息的结构体fs_struct,把计数users加1。
否则,在第14行代码中,新进程把当前进程的文件系统信息复制一份。
4)信号处理程序。只有属于同一个线程组的线程之间才会共享信号处理程序。函数copy_sighand复制或者共享信号处理程序,其代码如下:
kernel/fork.c
1 static int copy_sighand(unsigned long clone_flags, struct task_struct *tsk)
2 {
3 struct sighand_struct *sig;
4
5 if (clone_flags & CLONE_SIGHAND) {
6 atomic_inc(¤t->sighand->count);
7 return 0;
8 }
9 sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);
10 rcu_assign_pointer(tsk->sighand, sig);
11 if (! sig)
12 return -ENOMEM;
13
14 atomic_set(&sig->count, 1);
15 memcpy(sig->action, current->sighand->action, sizeof(sig->action));
16 return 0;
17 }
第5~8行代码,如果调用者传入标志CLONE_SIGHAND,表示共享信号处理程序,那么新进程和当前进程共享信号处理程序的结构体sighand_struct,把计数加1。
否则,在第9~15行代码中,新进程把当前进程的信号处理程序复制一份。
5)信号结构体。只有属于同一个线程组的线程之间才会共享信号结构体。函数copy_signal复制或共享信号结构体,其代码如下:
kernel/fork.c
1 static int copy_signal(unsigned long clone_flags, struct task_struct *tsk)
2 {
3 struct signal_struct *sig;
4
5 if (clone_flags & CLONE_THREAD)
6 return 0;
7
8 sig = kmem_cache_zalloc(signal_cachep, GFP_KERNEL);
9 tsk->signal = sig;
10 if (! sig)
11 return -ENOMEM;
12
13 sig->nr_threads = 1;
14 atomic_set(&sig->live, 1);
15 atomic_set(&sig->sigcnt, 1);
16
17 …
18 task_lock(current->group_leader);
19 memcpy(sig->rlim, current->signal->rlim, sizeof sig->rlim);
20 task_unlock(current->group_leader);
21 …
22 return 0;
23 }
第5行代码,如果调用者传入标志CLONE_THREAD,表示创建线程,那么新进程和当前进程共享信号结构体signal_struct。
否则,在第8~20行代码中,为新进程分配信号结构体,然后初始化,继承当前进程的资源限制。
6)虚拟内存。只有属于同一个线程组的线程之间才会共享虚拟内存。函数copy_mm复制或共享虚拟内存,其主要代码如下:
kernel/fork.c
1 static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
2 {
3 struct mm_struct *mm, *oldmm;
4 int retval;
5
6 …
7 tsk->mm = NULL;
8 tsk->active_mm = NULL;
9
10 oldmm = current->mm;
11 if (! oldmm)
12 return 0;
13
14 …
15 if (clone_flags & CLONE_VM) {
16 mmget(oldmm);
17 mm = oldmm;
18 goto good_mm;
19 }
20
21 retval = -ENOMEM;
22 mm = dup_mm(tsk);
23 if (! mm)
24 goto fail_nomem;
25
26 good_mm:
27 tsk->mm = mm;
28 tsk->active_mm = mm;
29 return 0;
30
31 fail_nomem:
32 return retval;
33 }
第15~19行代码,如果调用者传入标志CLONE_VM,表示共享虚拟内存,那么新进程和当前进程共享内存描述符mm_struct,把计数mm_users加1。
否则,在第22~28行代码中,新进程复制当前进程的虚拟内存。
7)命名空间。函数copy_namespaces创建或共享命名空间,其代码如下:
kernel/fork.c
1 int copy_namespaces(unsigned long flags, struct task_struct *tsk)
2 {
3 struct nsproxy *old_ns = tsk->nsproxy;
4 struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns);
5 struct nsproxy *new_ns;
6
7 if (likely(! (flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
8 CLONE_NEWPID | CLONE_NEWNET |
9 CLONE_NEWCGROUP)))) {
10 get_nsproxy(old_ns);
11 return 0;
12 }
13
14 if (! ns_capable(user_ns, CAP_SYS_ADMIN))
15 return -EPERM;
16
17 if ((flags & (CLONE_NEWIPC | CLONE_SYSVSEM)) ==
18 (CLONE_NEWIPC | CLONE_SYSVSEM))
19 return -EINVAL;
20
21 new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs);
22 if (IS_ERR(new_ns))
23 return PTR_ERR(new_ns);
24
25 tsk->nsproxy = new_ns;
26 return 0;
27 }
第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上下文,其代码如下:
kernel/fork.c
1 static int copy_io(unsigned long clone_flags, struct task_struct *tsk)
2 {
3 #ifdef CONFIG_BLOCK
4 struct io_context *ioc = current->io_context;
5 struct io_context *new_ioc;
6
7 if (! ioc)
8 return 0;
9
10 if (clone_flags & CLONE_IO) {
11 ioc_task_link(ioc);
12 tsk->io_context = ioc;
13 } else if (ioprio_valid(ioc->ioprio)) {
14 new_ioc = get_task_io_context(tsk, GFP_KERNEL, NUMA_NO_NODE);
15 if (unlikely(! new_ioc))
16 return -ENOMEM;
17
18 new_ioc->ioprio = ioc->ioprio;
19 put_io_context(new_ioc);
20 }
21 #endif
22 return 0;
23 }
第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的代码如下:
arch/arm64/kernel/process.c 1 int copy_thread(unsigned long clone_flags, unsigned long stack_start, 2 unsigned long stk_sz, struct task_struct *p) 3 { 4 struct pt_regs *childregs = task_pt_regs(p); 5 6 memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context)); 7 8 if (likely(! (p->flags & PF_KTHREAD))) { /* 用户进程 */ 9 *childregs = *current_pt_regs(); 10 childregs->regs[0] = 0; 11 12 /* 13 * 从寄存器tpidr_el0读取当前线程的线程本地存储的地址, 14 * 因为它可能和保存的值不一致 15 */ 16 *task_user_tls(p) = read_sysreg(tpidr_el0); 17 18 if (stack_start) { 19 if (is_compat_thread(task_thread_info(p))) 20 childregs->compat_sp = stack_start; 21 else 22 childregs->sp = stack_start; 23 } 24 25 /* 26 * 如果把线程本地存储的地址传给系统调用clone的第4个参数, 27 * 那么新线程将使用它 28 */ 29 if (clone_flags & CLONE_SETTLS) 30 p->thread.tp_value = childregs->regs[3]; 31 } else { /* 内核线程 */ 32 memset(childregs, 0, sizeof(struct pt_regs)); 33 childregs->pstate = PSR_MODE_EL1h; 34 if (IS_ENABLED(CONFIG_ARM64_UAO) && 35 cpus_have_const_cap(ARM64_HAS_UAO)) 36 childregs->pstate |= PSR_UAO_BIT; 37 p->thread.cpu_context.x19 = stack_start; 38 p->thread.cpu_context.x20 = stk_sz; 39 } 40 p->thread.cpu_context.pc = (unsigned long)ret_from_fork; 41 p->thread.cpu_context.sp = (unsigned long)childregs; 42 43 … 44 return 0; 45 }
执行过程如下。
第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 if (pid ! = &init_struct_pid) { 2 pid = alloc_pid(p->nsproxy->pid_ns_for_children); 3 if (IS_ERR(pid)) { 4 retval = PTR_ERR(pid); 5 goto bad_fork_cleanup_thread; 6 } 7 } 8 9 … 10 11 p->pid = pid_nr(pid); 12 if (clone_flags & CLONE_THREAD) { 13 p->exit_signal = -1; 14 p->group_leader = current->group_leader; 15 p->tgid = current->tgid; 16 } else { 17 if (clone_flags & CLONE_PARENT) 18 p->exit_signal = current->group_leader->exit_signal; 19 else 20 p->exit_signal = (clone_flags & CSIGNAL); 21 p->group_leader = p; 22 p->tgid = p->pid; 23 } 24 25 … 26 cgroup_threadgroup_change_begin(current); 27 retval = cgroup_can_fork(p); 28 if (retval) 29 goto bad_fork_free_pid; 30 31 write_lock_irq(&tasklist_lock); 32 33 if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) { 34 p->real_parent = current->real_parent; 35 p->parent_exec_id = current->parent_exec_id; 36 } else { 37 p->real_parent = current; 38 p->parent_exec_id = current->self_exec_id; 39 } 40 41 … 42 spin_lock(¤t->sighand->siglock); 43 … 44 if (likely(p->pid)) { 45 … 46 init_task_pid(p, PIDTYPE_PID, pid); 47 if (thread_group_leader(p)) { 48 init_task_pid(p, PIDTYPE_PGID, task_pgrp(current)); 49 init_task_pid(p, PIDTYPE_SID, task_session(current)); 50 51 if (is_child_reaper(pid)) { 52 ns_of_pid(pid)->child_reaper = p; 53 p->signal->flags |= SIGNAL_UNKILLABLE; 54 } 55 56 p->signal->leader_pid = pid; 57 p->signal->tty = tty_kref_get(current->signal->tty); 58 p->signal->has_child_subreaper = p->real_parent->signal-> has_child_ subreaper || 59 p->real_parent->signal->is_child_subreaper; 60 list_add_tail(&p->sibling, &p->real_parent->children); 61 list_add_tail_rcu(&p->tasks, &init_task.tasks); 62 attach_pid(p, PIDTYPE_PGID); 63 attach_pid(p, PIDTYPE_SID); 64 __this_cpu_inc(process_counts); 65 } else { 66 current->signal->nr_threads++; 67 atomic_inc(¤t->signal->live); 68 atomic_inc(¤t->signal->sigcnt); 69 list_add_tail_rcu(&p->thread_group, 70 &p->group_leader->thread_group); 71 list_add_tail_rcu(&p->thread_node, 72 &p->signal->thread_head); 73 } 74 attach_pid(p, PIDTYPE_PID); 75 nr_threads++; 76 } 77 78 total_forks++; 79 spin_unlock(¤t->sighand->siglock); 80 … 81 write_unlock_irq(&tasklist_lock); 82 83 proc_fork_connector(p); 84 cgroup_post_fork(p); 85 cgroup_threadgroup_change_end(current); 86 … 87 return p;
第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。
3.唤醒新进程
函数wake_up_new_task负责唤醒刚刚创建的新进程,其代码如下:
kernel/sched/core.c
1 void wake_up_new_task(struct task_struct *p)
2 {
3 struct rq_flags rf;
4 struct rq *rq;
5
6 raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
7 p->state = TASK_RUNNING;
8 #ifdef CONFIG_SMP
9 __set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
10 #endif
11 rq = __task_rq_lock(p, &rf);
12 update_rq_clock(rq);
13 post_init_entity_util_avg(&p->se);
14
15 activate_task(rq, p, ENQUEUE_NOCLOCK);
16 p->on_rq = TASK_ON_RQ_QUEUED;
17 …
18 check_preempt_curr(rq, p, WF_FORK);
19 #ifdef CONFIG_SMP
20 if (p->sched_class->task_woken) {
21 rq_unpin_lock(rq, &rf);
22 p->sched_class->task_woken(rq, p);
23 rq_repin_lock(rq, &rf);
24 }
25 #endif
26 task_rq_unlock(rq, p, &rf);
27 }
第7行代码,把新进程的状态从TASK_NEW切换到TASK_RUNNING。
第9行代码,在SMP系统上,创建新进程是执行负载均衡的绝佳时机,为新进程选择一个负载最轻的处理器。
第11行代码,锁住运行队列。
第12行代码,更新运行队列的时钟。
第13行代码,根据公平运行队列的平均负载统计值,推算新进程的平均负载统计值。
第15行代码,把新进程插入运行队列。
第18行代码,检查新进程是否可以抢占当前进程。
第22行代码,在SMP系统上,调用调度类的task_woken方法。
第26行代码,释放运行队列的锁。
4.新进程第一次运行
新进程第一次运行,是从函数ret_from_fork开始执行。函数ret_from_fork是由各种处理器架构自定义的函数,ARM64架构定义的ret_from_fork函数如下:
arch/arm64/kernel/entry.S 1 tsk .req x28 //当前进程的thread_info结构体的地址 2 3 ENTRY(ret_from_fork) 4 bl schedule_tail 5 cbz x19, 1f /* 如果寄存器x19的值是0,说明当前进程是用户进程,那么跳转到标号1 */ 6 mov x0, x20 /* 内核线程:x19存放线程函数的地址,x20存放线程函数的参数 */ 7 blr x19 /* 调用线程函数 */ 8 1: get_thread_info tsk /* 用户进程:x28 = sp_el0 = 当前进程的thread_info结构体的地址 */ 9 b ret_to_user /* 返回用户模式 */ 10 ENDPROC(ret_from_fork)
在介绍函数copy_thread时,我们已经说过:如果新进程是内核线程,寄存器x19存放线程函数的地址,寄存器x20存放线程函数的参数;如果新进程是用户进程,寄存器x19的值是0。
函数ret_from_fork的执行过程如下。
第4行代码,调用函数schedule_tail,为上一个进程执行清理操作。
第8行和第9行代码,如果寄存器x19的值是0,说明当前进程是用户进程,那么使用寄存器x28存放当前进程的thread_info结构体的地址,然后跳转到标号ret_to_user返回用户模式。
第6行和第7行代码,如果寄存器x19的值不是0,说明当前进程是内核线程,那么调用线程函数。
函数schedule_tail负责为上一个进程执行清理操作,是新进程第一次运行时必须最先做的事情,其代码如下:
kernel/sched/core.c
1 asmlinkage __visible void schedule_tail(struct task_struct *prev)
2 __releases(rq->lock)
3 {
4 struct rq *rq;
5
6 rq = finish_task_switch(prev);
7 balance_callback(rq);
8 preempt_enable();
9
10 if (current->set_child_tid)
11 put_user(task_pid_vnr(current), current->set_child_tid);
12 }
函数schedule_tail的执行过程如下。
第6行代码,调用函数finish_task_switch(),为上一个进程执行清理操作,参考2.8.6节。
第7行代码,执行运行队列的所有负载均衡回调函数。
第8行代码,开启内核抢占。
第10行和第11行代码,如果pthread库在调用clone()创建线程时设置了标志位CLONE_CHILD_SETTID,那么新进程把自己的进程标识符写到指定位置。