Linux内核创建一个新进程
张雨梅 原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-10000
创建新进程
如果同一个程序被多个用户同时运行,那么这个程序就有多个相对独立的进程,与此同时他们又共享相同的执行代码。在Linux系统中进程的概念类似于任务或者线程
创建进程,调用fork函数,这是一个系统函数。fork函数与系统函数的调用大体相同,但是fork之后产生了一个新的进程,会发生两次返回。
task_struct的数据结构
每个可以独立被调度的执行上下文都有一个进程描述符,不管是进程还是线程都有自己的 task_struct结构
task_struct的定义链接是http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#task_struct,其代码很长,下面就列出一部分
1235struct task_struct { 1236 volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped 描述进程运行状态*/ 1237 void *stack; //指定进程的内核堆栈 1238 atomic_t usage; 1239 unsigned int flags; /* per process flags, defined below */ 1240 unsigned int ptrace; 1241 1242#ifdef CONFIG_SMP 1243 struct llist_node wake_entry; 1244 int on_cpu; 1245 struct task_struct *last_wakee; 1246 unsigned long wakee_flips; 1247 unsigned long wakee_flip_decay_ts; 1248 1249 int wake_cpu; 1250#endif 1251 int on_rq; //运行队列 1252 1253 int prio, static_prio, normal_prio; //定义优先级 1254 unsigned int rt_priority; 1255 const struct sched_class *sched_class; //进程调度相关的定义 1256 struct sched_entity se; 1257 struct sched_rt_entity rt; 1258#ifdef CONFIG_CGROUP_SCHED 1259 struct task_group *sched_task_group; 1260#endif 1261 struct sched_dl_entity dl;
......
linux进程的状态有
task_running,就绪态、运行态都用这个表示,这与操作系统中的定义不同,这种定义方式表示进程是可运行的,就绪状态不同的是要等待cpu的调度。
task_zombie,僵尸状态,即死锁
task_interruptible或task_uninterruptible,阻塞状态
还有_task_stopped,_task_traced,exit_zombie,exit_dead等各种状态。
PCB中指定了内核栈,没有指定用户栈,这是因为进程由用户态进入内核态时,保存了用户态进程的现场,恢复现场时,即可找到用户栈。
1295struct list_head tasks;//进程链表,链接所有当前的进程 1296#ifdef CONFIG_SMP 1297 struct plist_node pushable_tasks; 1298 struct rb_node pushable_dl_tasks; 1299#endif
其中list_head是一个双向链表,表示当前进程的前驱和后继关系,其定义是
struct list_head{ struct list_head *next,*prev; };
1301struct mm_struct *mm, *active_mm;
这是进程的地址空间管理,与数据段、代码段、分页、分段、物理空间用户逻辑空间的转换有关,这里忽略掉这些细节。Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈。
1330 pid_t pid; 1331 pid_t tgid;
pid,process id
tgid,thread group id
linux给每一个进程和轻量级进程都分配一个pid,程序员希望向进程发送信号时,此信号可以影响进程及进程产生的轻量级进程,因此产生了线程组(可以理解为轻量级进程组)的概念,在线程组内,每个线程都使用此线程组内第一个线程的pid,并将此值存入tgid。也就是说一个组内的线程的tgid相同。
1333#ifdef CONFIG_CC_STACKPROTECTOR 1334 /* Canary value for the -fstack-protector gcc feature */ 1335 unsigned long stack_canary; 1336#endif
stack-canary是设置的栈警卫,可以用来保护栈防止被攻击,它所在的地址如果被改写,就认为栈受到了攻击。
1342struct task_struct __rcu *real_parent; /* real parent process */ 1343 struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */ 1344 /* 1345 * children/sibling forms the list of my natural children 1346 */ 1347 struct list_head children; /* list of my children */ 1348 struct list_head sibling; /* linkage in my parent's children list */ 1349 struct task_struct *group_leader; /* threadgroup leader */ ......
这一段定义了进程的父子关系,组等,list-head是进程链表,含有前驱、后继的设置,用来表示进程的父子关系,ptraced做调试使用, 前面介绍的tgid的值就是*group_leader的进程号。
1368cputime_t utime, stime, utimescaled, stimescaled; 1369 cputime_t gtime; 1370#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE 1371 struct cputime prev_cputime; 1372#endif
......//和时间相关的一些定义
/* CPU-specific state of this task */ 1412 struct thread_struct thread;
这个是和cpu相关的状态,其中包括sp、ip、es、fs、gs、error_code等。是一个比较重要的数据结构。其他定义这里先不分析。
fork函数对应的内核处理过程sys_clone
fork函数用于创建子进程,创建的进程与当前的进程是一个复制的过程,但是中间也会有修改,比如进程的id等。创建子进程之后,子进程与父进程可以说是独立的,可以各自运行。创建进程要发生系统调用,调用system_call,执行相应的系统函数,这里就是sys_clone。fork()调用执行一次返回两个值,对于父进程,fork函数返回子程序的进程号,而对于子程序,fork函数则返回零,这就是fork函数的一次调用,两次返回。
1 int main(int argc, char * argv[]) 2 { 3 int pid; 4 /* fork another process */ 5 pid = fork(); 6 if (pid < 0) 7 { 8 /* error occurred */ 9 fprintf(stderr,"Fork Failed!"); 10 exit(-1); 11 } 12 else if (pid == 0) 13 { 14 /* child process */ 15 printf("This is Child Process!\n"); 16 } 17 else 18 { 19 /* parent process */ 20 printf("This is Parent Process!\n"); 21 /* parent will wait for the child to complete*/ 22 wait(NULL); 23 printf("Child Complete!\n"); 24 } 25 }
比如上面的程序,pid=0,表示子进程,pid>0表示父进程,在子进程中,执行printf("This is Child Process!\n");在父进程中,执行printf("This is Parent Process!\n");也就是说这里的if,else两种情况都会执行到。
fork创建的子进程,从用户态的角度看,是从fork的下一句命令执行。在内核中,fork是一个新创建的进程,从my_process开始执行。
linux中进程创建有三种方法fork,vfork,clone。
fork,子进程只是完全复制父进程的资源,子进程有自己的task_struct结构和pid, 数据空间,堆和栈的副本都是复制父进程的。
vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之)或exit。vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。
clone()带有参数,fork()和vfork()是无参数的,fork()是全部复制,vfork()是共享内存,clone()是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags来决定。另外,clone()返回的是子进程的pid。
1703SYSCALL_DEFINE0(fork) 1704{ 1705#ifdef CONFIG_MMU 1706 return do_fork(SIGCHLD, 0, 0, NULL, NULL); 1707#else 1708 /* can not support in nommu mode */ 1709 return -EINVAL; 1710#endif 1711}
1715SYSCALL_DEFINE0(vfork) 1716{ 1717 return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 1718 0, NULL, NULL); 1719}
1740SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, 1741 int __user *, parent_tidptr, 1742 int __user *, child_tidptr, 1743 int, tls_val) 1744#endif 1745{ 1746 return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr); 1747} 1748#endif
可以看到fork,vfork,clone函数都会调用do_fork函数。clone函数有些参数,linux定义了多种参数不同的clone函数。下面看一下do_fork的代码。
1623long do_fork(unsigned long clone_flags, 1624 unsigned long stack_start, 1625 unsigned long stack_size, 1626 int __user *parent_tidptr, 1627 int __user *child_tidptr) 1628{ 1629 struct task_struct *p; 1630 int trace = 0; 1631 long nr; ...... 1651 p = copy_process(clone_flags, stack_start, stack_size, 1652 child_tidptr, NULL, trace);
do_fork实现进程的创建,调用copy_process,
static struct task_struct *copy_process(unsigned long clone_flags, 1183 unsigned long stack_start, 1184 unsigned long stack_size, 1185 int __user *child_tidptr, 1186 struct pid *pid,1187 int trace) 1188{ 1189 int retval; 1190 struct task_struct *p; 1191 1192 if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))//出错处理 1193 return ERR_PTR(-EINVAL); ...... 1235 retval = security_task_create(clone_flags); 1236 if (retval) 1237 goto fork_out; 1238 1239 retval = -ENOMEM; 1240 p = dup_task_struct(current);//复制进程的pcb 1241 if (!p) 1242 goto fork_out; 1243 1244 ftrace_graph_init_task(p); 1245 1246 rt_mutex_init_task(p); 1247
......
1288 p->utime = p->stime = p->gtime = 0;//修改子进程的参数,也是初始化 1289 p->utimescaled = p->stimescaled = 0; 1290#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE 1291 p->prev_cputime.utime = p->prev_cputime.stime = 0; 1292#endif 1293#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN 1294 seqlock_init(&p->vtime_seqlock); 1295 p->vtime_snap = 0; 1296 p->vtime_snap_whence = VTIME_SLEEPING; 1297#endif
copy_process是创建进程的主要代码。前面定义了一些出错处理实现进程的复制,同时修改子进程中的一些信息,pid等。其中有copy_thread,包含thread的sp、ip等。然后调用dup_task_struct函数。
305static struct task_struct *dup_task_struct(struct task_struct *orig) 306{ 307 struct task_struct *tsk;// 308 struct thread_info *ti; 309 int node = tsk_fork_get_node(orig); 310 int err; 311 312 tsk = alloc_task_struct_node(node);//分配一个结点空间 313 if (!tsk) 314 return NULL; 315 316 ti = alloc_thread_info_node(tsk, node);//分配一个进程内核堆栈空间,创建了一个页面,一部分存放thread_info,一部分存放内核堆栈(从高地址向低地址) 317 if (!ti) 318 goto free_tsk; 319 320 err = arch_dup_task_struct(tsk, orig);//复制orig进程的信息给当前进程 321 if (err) 322 goto free_ti;
复制进程的时候会调用arch_dup_task_struct,功能是把一个指针赋给另一个指针,实现的股票功能就是把orig赋给tsk。
290int __weak arch_dup_task_struct(struct task_struct *dst, 291 struct task_struct *src) 292{ 293 *dst = *src; 294 return 0; 295}
然后执行dup_task_struct函数,复制父进程的内核堆栈,copy_thread函数,拷贝寄存器信息,esp,eip等,然后到ret_from_fork,是子进程在内核中执行的起点。
首先修改实验楼的menuos操作系统,添加fork系统函数。在实验楼的虚拟机上进行实验,在终端输入命令,可以看到menuo已经包含fork指令。
用gdb调试观察fork的执行过程,在终端输入命令:
qemu -kernel linux3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
重新打开一个终端,用gdb调试
gdb
file linux-3.18.6/vmlinux
target remote:1234
设置断点为:sys_clone, do _fork, copy_process, dup_task_struct, copy_thread, ret_from_fork
在本次实验的menuos环境中,fork, vfork, clone调用的是sys_clone,不是sys_fork, 然后都会调用do_fork.
观察到fork一个子进程时,其执行过程是
总结
进程控制块PCB,包含了进程中的各种信息,pid,状态,内核栈,进程链表,调度相关的定义,内存管理,文件系统等,内容很强大很复杂。fork产生一个子进程,与父进程的情况大概相同,之后子进程与父进程是相互独立的。在父进程中的返回和一般系统调用一样,在子进程返回到ret_from_fork。
参考资料:http://blog.sina.com.cn/s/blog_7673d4a5010103x7.html fork,vfork,clone