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

posted @ 2015-04-12 23:03  蘑菇糖  阅读(2069)  评论(0编辑  收藏  举报