1.进程的创建
进程的创建是一个非常复杂的过程,这里以用户空间的fork为出手点,去探究进程在在内核中的创建流程。
1.1.进程的命名空间
我们知道每个进程都有自己唯一的一个pid,在内核中都有自己唯一的一个task_struct,那么内核中是如何为一个进程分配一个唯一的pid的呢?
我们来看一下内核中是如何实现的:struct task_struct{ //...... pid_t pid; pid_t tgid; struct pid_link pids[PIDTYPE_MAX]; //.....}struct pid_link{ struct hlist_node node; struct pid *pid;}; struct pid{ atomic_t count; unsigned int level; /* lists of tasks that use this pid */ struct hlist_head tasks[PIDTYPE_MAX]; struct rcu_head rcu; struct upid numbers[1];};struct upid { /* Try to keep pid_chain in the same cacheline as nr for find_vpid */ int nr; //PID struct pid_namespace *ns; struct hlist_node pid_chain; // pid hash 散列表节点};
每个进程的task_struct结构体中有一个指向pid结构体的指针,pid结构体中包含了PID号。
说明:
1.已知task_struct结构体,根据其pid_link的pid指针找到pid结构体,取出其nr即为PID号。 2.Pid_hash[]:这是一个hash表的结构,根据pid的hr值哈希到其某个表项,这里解决冲突使用的是散列表法。 3.那么,如何根据PID号快速的找到task_struct结构体呢?
- 首先通过PID计算pid挂接到哈希表pid_hash[]的表项。
- 遍历该表项,找到pid结构体中nr值和PID号相同的哪个pid结构体。
- 再通过该pid 结构体中的tasks指针找到node
- 最后根据内核的container_of机制就能找到task_struct结构体。(这个没有懂啊)
4.pid_map:这是一个位图,用来唯一分配PID值的结构,图中灰色表示已经分配过的值,在新建一个进程时,只需在其中找到一个为分配过的值赋给 pid 结构体的 nr,再将pid_map 中该值设为已分配标志。这也就解决了上面的第3个问题——如何快速地分配一个全局的PID。
1.2进程ID有类型之分
如果只是为了从PID找到task_struct或者从task_struct分配一个PID,那么上面复杂的数据结构显然是不需要的,完全可以用下面的代替:
那么内核为何要如此的设计各个结构体之间的关系呢?
我们都知道,进程之间有复杂的关系,如线程组,进程组,会话组。这些组均有组ID,在内核中为一个枚举类型,分别为PID, PGID, SID。所以要在task_struct中的pid_link指向一个pid结构体时,增加几项来指向其组长的pid结构体。相应的struct pid 原本只需要指回PID所属进程的task_struct,现在也要增加几项,用来链接那些以该pid为组长的所有进程组内的进程。所以就有了上面内核中的数据结构。 假设现在有三个进程A、B、C为同一个进程组,进程组长为A,这样的结构图为:说明:
1.进程B和C的进程组组长为A,那么在B和C进程中的pids[PIDTYPE_PGID]中有一个指针指向A进程的pid结构体。 2.进程A是进程B和C的组长,进程A的 pid 结构体的 tasks[PIDTYPE_PGID] 是一个散列表的头,它将所有以该pid 为组长的进程链接起来。1.3进程命名空间的数据结构体图(参考内核架构一书)
2、用户进程的创建
用户态在创建一个进程时,只要fork就可以了,那么具体的fork都做了那些事呢?
fork()系统调用对应的内核实现为sys_fork(),sys_fork()是对do_fork()的简单封装,sys_fork()的任务是从处理器寄存器中提取由用户空间提供的信息,do_fork()负责进程的复制。我们从do_fork的调用流程就可以发现,最重要的两个函数就是:copy_process()和wake_up_new_task()这两个函数。
现在我们来看下copy_process的执行流程:2.1.标志检查
检查clone_flags中指定的标志是否冲突以及安全检查。clone_flags是一个标志集合,分为两部分:最低的字节指定了在子进程终止时发送给父进程的信号,其余的高位字节保存了各种真正的复制标志,如CLONE_FS、CLONE_THREAD等。当我们在用户态下使用fork时,不能指定标志,所以默认的clone_flags的值为SIGCHILD,所以这里的检查肯定是没有问题的。(你如果像修改子进程的退出信号,你可以使用clone函数)。security_task_create是安全性检查,询问Linux Security Moudule(LSM)看当前任务是否可以创建一个新任务。2.2dup_task_struct 这个函数为子进程分配一个内核栈,thread_info结构和task_struct结构体。 prepare_to_copy():为正式的拷贝做一些准备工作。主要是将此一些必要的寄存器的值保存到父进程的thread_info结构中,这些值稍后会被复制到子进程的thread_info结构中。 alloc_task_struct():该函数返回的是一个task_struct结构体类型的变量,所以显而易见,这是分配了一个新的task_struct结构体。内部调用kmalloc来分配内存。 alloc_thread_info():thread_info结构体中保存了特定于体系结构的汇编语言代码需要访问的部分进程数据,包括执行域,课抢占标志和当前的CPU信息等。其实调用的是__get_free_page()函数,分配两个物理页面,很显然thread_info结构是用不了2页的,剩余的内存其实是用作子进程的内核栈。(不懂) arch_dup_task_struct():dst = src,拷贝父进程的task_struct到子进程的task_struct。 setup_thread_stack():将父进程中的thread_info拷贝给子进程中的thread_info. atomic_set():原子操作,一些类似于同步的操作。2.3.资源检查
我们知道Linux是多用户操作系统,每个用户可以使用的资源也是有限制的,包括可以创建的进程数。所以创建进程的过程是否继续进行取决于下面的检查:第一个检查是系统对每个用户可以创建的进程数限制,默认是1024。这个限制可以通过ulimit()系统调用或ulimit命令修改。除了修改限制外,创建进程的用户如果是root用户或具有CAP_SYS_ADMIN或CAP_SYS_RESOURCE权限则不受这个限制。
第二个检查是当前的进程(线程属于轻量级进程)数nr_threads是否超过最大的进程数max_threads。max_threads是一个全局变量,在fork_init()中初始化,这个值是根据系统中的物理页面数计算出来的,如下所示:
max_threads = mempages / (8 * THREAD_SIZE / PAGE_SIZE);
另外还有一点要注意,第一个检查中使用的是子进程的p->real_cred->user结构来检查,但是在前面的代码中并没有发现有初始化real_cred成员的地方。我在看到这个地方的时候非常的疑惑,后来才找到在arch_dup_task_struct()中通过dst = src;语句来拷贝父进程的所有内容。所以这里的比较,虽然使用的是子进程的描述符结构,但是其实还是使用的是父进程的数据。不过在copy_creds()中可能会给子进程分配新的cred结构。
2.4.拷贝资源
if ((retval = copy_semundo(clone_flags, p))) goto bad_fork_cleanup_audit; if ((retval = copy_files(clone_flags, p))) goto bad_fork_cleanup_semundo; if ((retval = copy_fs(clone_flags, p))) goto bad_fork_cleanup_files; if ((retval = copy_sighand(clone_flags, p))) goto bad_fork_cleanup_fs; if ((retval = copy_signal(clone_flags, p))) goto bad_fork_cleanup_sighand; if ((retval = copy_mm(clone_flags, p))) goto bad_fork_cleanup_signal; if ((retval = copy_namespaces(clone_flags, p))) goto bad_fork_cleanup_mm; if ((retval = copy_io(clone_flags, p))) goto bad_fork_cleanup_namespaces;
包括对内存,文件描述符,信号等的拷贝。
2.5.子进程创建后,肯定要加入CPU的执行队列,这样才可能被执行
这里就是调用wake_up_new_task()函数来实现,这是调度器与进程创建的第二个逻辑交互时机,内核会调用调度器类的task_new函数(sched_class结构中),将新进程加入到相应的类的就绪队列。3,关于thread_info结构体
每当进程从用户态进入内核态后都要使用栈,这个栈叫做进程的内核栈。当进程已进入内核栈,CPU就自动设置该进程的内核栈,这个栈位于内核的数据段上。为了节省空间,linux把内核栈和一个紧挨近PCB的小数据结构thread_info放在一起,占用8KB的内存空间。
在dup_task_struct中会调用alloc_thread_info分配两个页(8KB)的空闲内存(其内部调用的是一个__get_free_psages函数)。 首先来看一下内核中的数据结构struct thread_info { struct pcb_struct pcb; /* palcode state */ struct task_struct *task; /* main task structure */ unsigned int flags; /* low level flags */ unsigned int ieee_state; /* see fpu.h */ struct exec_domain *exec_domain; /* execution domain */ mm_segment_t addr_limit; /* thread address space */ unsigned cpu; /* current CPU */ int preempt_count; /* 0 => preemptable, <0 => BUG */ int bpt_nsaved; unsigned long bpt_addr[2]; /* breakpoint handling */ unsigned int bpt_insn[2]; struct restart_block restart_block;};
thread_info结构体中并不直接包含于进程相关的字段,而是通过task字段指向具体某个进程的描述符。进程内核栈和thread_info结构体之间的逻辑关系:
从上图可知,内核栈是从该内存区域的顶层向下(从高地址到低地址)增长的,而thread_info结构则是从该区域的开始处向上(从低地址到高地址)增长。内核栈的栈顶地址存储在esp寄存器中。所以,当进程从用户态切换到内核态后,esp寄存器指向这个区域的末端。
从代码的角度来看,内核栈和thread_info结构是被定义在一个联合体当中的:union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)];};
其中,THREAD_SIZE的值取8192时,stack数组的大小为2048;THREAD_SIZE的值取4096时,stack数组的大小为1024。现在我们应该思考,为何要将内核栈和thread_info(其实也就相当于task_struct,只不过使用thread_info结构更节省空间)紧密的放在一起?最主要的原因就是内核可以很容易的通过esp寄存器的值获得当前正在运行进程的thread_info结构的地址,进而获得当前进程描述符的地址。
/* how to get the thread information struct from C */static inline struct thread_info *current_thread_info(void){ struct thread_info *ti; __asm__("extui %0,a1,0,13\n\t" "xor %0, a1, %0" : "=&r" (ti) : ); return ti;}
这条内联汇编语句会屏蔽掉esp寄存器中的内核栈顶地址的低13位(或12位,当THREAD_SIZE为4096时)。此时ti所指的地址就是这片内存区域的起始地址,也就刚好是thread_info结构的地址。但是,thread_info结构的地址并不会对我们直接有用。我们通常可以轻松的通过current宏获得当前进程的task_struct结构,这个宏是如何实现的?
#define get_current() (current_thread_info()->task)#define current get_current()
通过上述源码可以发现,current宏返回的是thread_info结构task字段。而task正好指向与thread_info结构关联的那个进程描述符。得到current后,我们就可以获得当前正在运行进程的描述符中任何一个字段了,比如我们通常所做的:current->pid。
thread_info结构体的作用
thread_info结构并不代表与线程相关的信息,而是表示与硬件更紧密的一些数据。thread_info与task_struct结构体中都有一个域指向对方,因此事一一对应的关系。之所以定义一个thread_info结构,原因之一可能是,进程控制(task_struct)块的成员中引用最为频繁的是thread_info结构体中的内容。