text 段,主要保存着程序的代码对应的机器指令,这也将会是 CPU 所将要执行的机器指令的集合。text 段是可共享的,所以对于经常执行的程序只需保留一份 text 段的拷贝在内存中就可以了。特别地,text 段是只读的,进程无法对 text 段进行修改,这样可以防止一个进程意外地修改它自己的指令。
在 Linux 操作系统中,每个进程被创建的时候,内核会给这个进程分配一个进程描述符结构。进程描述符在一般的操作系统概念中也被称为 PCB ,也就是进程控制块。这个进程描述符保存了这个进程的状态,标识符,打开的文件,等待的信号,文件系统等待的资源信息。每个进程描述符都表示了独立的一个进程,而在系统中,每个进程的进程描述都加入到一个双向循环的任务队列中,由操作系统进行进程的调度,决定哪个进程可以占用 CPU ,哪个进程应该让出 CPU 。Linux 中的进程描述符是一个 task_struct 类型的结构体。在 Linux 中,一个进程的进程描述符结构如下图所示:
我们上面提到,操作系统将硬件资源和应用程序隔离开来,那应用程序如果需要操作一些硬件或者获取一些资源如何实现?答案是内核提供了一系列的服务比如 IO 或者进程管理等给应用程序调用,也就是通过系统调用( system call )。如下图:
系统调用实际上就是函数调用,也是一系列的指令的集合。和普通的应用程序不同,系统调用是运行在内核空间的。当应用程序调用系统调用的时候,将会从用户空间切换到内核空间运行内核的代码。不同的架构实现内核调用的方式不同,在 i386 架构上,运行在用户空间的应用程序如果需要调用相关的系统调用,可以首先把系统调用编号和参数存放在相关的寄存器中,然后使用0x80这个值来执行软中断 int 。软中断发生之后,内核根据寄存器中的系统调用编号去执行相关的系统调用指令。
正如上面的图所展示的,应用程序可以直接通过系统调用接口调用内核提供的系统调用,也可以通过调用一些 C 库函数,而这些 C 库函数实际上是通过系统调用接口调用相关的系统调用。C 库函数有些在调用系统调用前后做一些特别的处理,但也有些函数只是单纯地对系统调用做了一层包装。
1.5 fork 系统调用
fork 系统调用是 Linux 中提供的众多系统调用中的一个,是2号系统调用。在 Linux 中,需要一种机制来创建新的进程,而 fork 就是 Linux 中提供的一个从旧的进程中创建新的进程的方法。我们在编程中,一般是调用 C 库的 fork 函数,而这个 fork 函数则是直接包装了 fork 系统调用的一个函数。fork 函数的效果是对当前进程进行复制,然后创建一个新的进程。旧进程和新进程之间是父子关系,父子进程共享了同一个 text 段,并且父子进程被创建后会从 fork 函数调用点下一个指令继续执行。fork 函数有着一次调用,两次返回的特点。在父进程中,fork 调用将会返回子进程的 PID ,而在子进程中,fork 调用返回的是0。之所以这样处理是因为进程描述符中保存着父进程的 PID ,所以子进程可以通过 getpid 来获取父进程的 PID,而进程描述符中却没有保存子进程的 PID 。
fork系统调用的调用过程简单描述如下:
首先是开始,父进程调用 fork ,因为这是一个系统调用,所以会导致 int 软中断,进入内核空间;
本部分内容主要是对相关的具体源码进行分析,使用的 Linux 内核源码版本为3.6.11。被分析的源码并不是全部的相关源码,只是相关源码的一些重要部分。
2.1 进程描述符
在 Linux 中,进程描述符是一个 task_struct 类型的数据结构,这个数据结构的定义是在 Linux 源码的 include/linux/sched.h 中。
1234567
structtask_struct{volatilelongstate;/* -1 unrunnable, 0 runnable, >0 stopped */void*stack;atomic_tusage;unsignedintflags;/* per process flags, defined below */unsignedintptrace;...
/* * Scheduling policies */#defineSCHED_NORMAL0#defineSCHED_FIFO1#defineSCHED_RR2#defineSCHED_BATCH3/* SCHED_ISO: reserved but not implemented yet */#defineSCHED_IDLE5
也就是有这几种调度策略:
SCHED_NORMAL,用于普通进程;
SCHED_FIFO,先来先服务;
SCHED_RR,时间片轮转调度;
SCHED_BATCH,用于非交互的处理器消耗型进程;
SCHED_IDLE,主要是在系统负载低的时候使用。
一个进程还包括了各种的标识符,用来标识某一个特定的进程,同时也用来标识这个进程所属的进程组。如下:
1234
...pid_tpid;pid_ttgid;...
同时,在 task_struct 中也定义了一些特别指向其他进程的指针。
123456789101112131415
.../* * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->real_parent->pid) */structtask_struct__rcu*real_parent;/* real parent process */structtask_struct__rcu*parent;/* recipient of SIGCHLD, wait4() reports *//* * children/sibling forms the list of my natural children */structlist_headchildren;/* list of my children */structlist_headsibling;/* linkage in my parent's children list */structtask_struct*group_leader;/* threadgroup leader */...
...cputime_tutime,stime,utimescaled,stimescaled;cputime_tgtime;...unsignedlongnvcsw,nivcsw;/* context switch counts */structtimespecstart_time;/* monotonic time */structtimespecreal_start_time;/* boot based time *//* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */unsignedlongmin_flt,maj_flt;structtask_cputimecputime_expires;structlist_headcpu_timers[3];...
一个进程,从创建到结束,这是它的生命周期。在进程生命周期中有许多与时间相关的内容,这些内容也包括在进程描述符中了。如上代码,我们可以看到有好几个数据类型为 cputime 的成员。utime 和 stime 分别表示进程在用户态下使用 CPU 的时间和在内核态下使用 CPU 的时间,这两个成员的单位是一个 click 。而 utimescaled 和 stimescaled 同样也是分别表示进程在这两种状态下使用 CPU 的时间,只不过单位是处理器的频率。 gtime 表示的是虚拟处理器的运行时间。start_time 和 real_start_time 表示的都是进程的创建时间,real_start_time 包括了进程睡眠的时间。cputime_expires 表示的是进程或者进程组被跟踪的 CPU 时间,对应着 cpu_timers 的三个值。
1234
/* filesystem information */structfs_struct*fs;/* open file information */structfiles_struct*files;
/* * "alpha_clone()".. By the time we get here, the * non-volatile registers have also been saved on the * stack. We do some ugly pointer stuff here.. (see * also copy_thread) * * Notice that "fork()" is implemented in terms of clone, * with parameters (SIGCHLD, 0). */intalpha_clone(unsignedlongclone_flags,unsignedlongusp,int__user*parent_tid,int__user*child_tid,unsignedlongtls_value,structpt_regs*regs){if(!usp)usp=rdusp();returndo_fork(clone_flags,usp,regs,0,parent_tid,child_tid);}
/* * Ok, this is the main fork-routine. * * It copies the process, and if successful kick-starts * it and waits for it to finish using the VM if required. */longdo_fork(unsignedlongclone_flags,unsignedlongstack_start,structpt_regs*regs,unsignedlongstack_size,int__user*parent_tidptr,int__user*child_tidptr){structtask_struct*p;inttrace=0;longnr;
/* * cloning flags: */#defineCSIGNAL0x000000ff/* signal mask to be sent at exit */#defineCLONE_VM0x00000100/* set if VM shared between processes */#defineCLONE_FS0x00000200/* set if fs info shared between processes */#defineCLONE_FILES0x00000400/* set if open files shared between processes */#defineCLONE_SIGHAND0x00000800/* set if signal handlers and blocked signals shared */#defineCLONE_PTRACE0x00002000/* set if we want to let tracing continue on the child too */#defineCLONE_VFORK0x00004000/* set if the parent wants the child to wake it up on mm_release */#defineCLONE_PARENT0x00008000/* set if we want to have the same parent as the cloner */#defineCLONE_THREAD0x00010000/* Same thread group? */#defineCLONE_NEWNS0x00020000/* New namespace group? */#defineCLONE_SYSVSEM0x00040000/* share system V SEM_UNDO semantics */#defineCLONE_SETTLS0x00080000/* create a new TLS for the child */#defineCLONE_PARENT_SETTID0x00100000/* set the TID in the parent */#defineCLONE_CHILD_CLEARTID0x00200000/* clear the TID in the child */#defineCLONE_DETACHED0x00400000/* Unused, ignored */#defineCLONE_UNTRACED0x00800000/* set if the tracing process can't force CLONE_PTRACE on this clone */#defineCLONE_CHILD_SETTID0x01000000/* set the TID in the child */
CLONE_VM 表示在父子进程间共享 VM ;
CLONE_FS 表示在父子进程间共享文件系统信息,包括工作目录等;
CLONE_FILES 表示在父子进程间共享打开的文件;
CLONE_SIGHAND 表示在父子进程间共享信号的处理函数;
CLONE_PTRACE 表示如果父进程被跟踪,子进程也被跟踪;
CLONE_VFORK 在 vfork 的时候使用;
CLONE_PARENT 表示和复制的进程有同样的父进程;
CLONE_THREAD 表示同一个线程组;
之前提到过,在 Linux 中,线程的实现是和进程统一的,就是说,在 Linux 中,进程和线程的结构都是 task_struct 。区别在于,多个线程会共享一个进程的资源,包括虚拟地址空间,文件系统,打开的文件和信号处理函数。线程的创建和一般的进程的创建差不多,区别在于调用 clone 系统调用时,需要通过传入相关的标志参数指定要共享的特定资源。通常是这样的:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)。
/* * Do some preliminary argument and permissions checking before we * actually start allocating stuff */if(clone_flags&CLONE_NEWUSER){if(clone_flags&CLONE_THREAD)return-EINVAL;/* hopefully this check will go away when userns support is * complete */if(!capable(CAP_SYS_ADMIN)||!capable(CAP_SETUID)||!capable(CAP_SETGID))return-EPERM;}
/* * Determine whether and which event to report to ptracer. When * called from kernel_thread or CLONE_UNTRACED is explicitly * requested, no event is reported; otherwise, report if the event * for the type of forking is enabled. */if(likely(user_mode(regs))&&!(clone_flags&CLONE_UNTRACED)){if(clone_flags&CLONE_VFORK)trace=PTRACE_EVENT_VFORK;elseif((clone_flags&CSIGNAL)!=SIGCHLD)trace=PTRACE_EVENT_CLONE;elsetrace=PTRACE_EVENT_FORK;if(likely(!ptrace_event_enabled(current,trace)))trace=0;}
p->did_exec=0;delayacct_tsk_init(p);/* Must remain after dup_task_struct() */copy_flags(clone_flags,p);INIT_LIST_HEAD(&p->children);INIT_LIST_HEAD(&p->sibling);rcu_copy_process(p);p->vfork_done=NULL;spin_lock_init(&p->alloc_lock);init_sigpending(&p->pending);
这段代码首先将进程描述符p的did_exec值设置为0,以保证这个新创建的进程不会被运行。因为子进程和父进程实际上还是有区别的,所以,接着将会将子进程的进程描述符的部分内容清除掉并设置为初始的值。如上,新创建的进程的描述符中 children ,sibling 和等待的信号等值都被初始化了。然后,这段代码还调用了 copy_flags 函数,copy_flags 函数如下:
retval=perf_event_init_task(p);if(retval)gotobad_fork_cleanup_policy;retval=audit_alloc(p);if(retval)gotobad_fork_cleanup_policy;/* copy all the process information */retval=copy_semundo(clone_flags,p);if(retval)gotobad_fork_cleanup_audit;retval=copy_files(clone_flags,p);if(retval)gotobad_fork_cleanup_semundo;retval=copy_fs(clone_flags,p);if(retval)gotobad_fork_cleanup_files;retval=copy_sighand(clone_flags,p);if(retval)gotobad_fork_cleanup_fs;retval=copy_signal(clone_flags,p);if(retval)gotobad_fork_cleanup_sighand;retval=copy_mm(clone_flags,p);if(retval)gotobad_fork_cleanup_signal;retval=copy_namespaces(clone_flags,p);if(retval)gotobad_fork_cleanup_mm;retval=copy_io(clone_flags,p);if(retval)gotobad_fork_cleanup_namespaces;retval=copy_thread(clone_flags,stack_start,stack_size,p,regs);if(retval)gotobad_fork_cleanup_io;
if(!IS_ERR(p)){...wake_up_new_task(p);/* forking complete and child started to run, tell ptracer */if(unlikely(trace))ptrace_event(trace,nr);if(clone_flags&CLONE_VFORK){if(!wait_for_vfork_done(p,&vfork))ptrace_event(PTRACE_EVENT_VFORK_DONE,nr);}
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>#defineLEN1024*1024intmain(intargc,char**argv){pid_tpid;intnum=10,i;char*p;p=malloc(LEN*sizeof(char));pid=fork();if(pid>0){/*parent process.*/printf("parent %d process get %d!It stores in %x.\n",getpid(),num,&num);printf("parent have a piece of memory start from %x.\n",p);}else{/*child process.*/printf("child %d process get %d!It stores in %x.\n",getpid(),num,&num);printf("child have a piece of memory start from %x.\n",p);}while(1){}return0;}
这个程序只是简单地调用了一次 fork ,创建了一个子进程,然后分别在父子进程中查看申请的一块内存的起始地址。此外还添加了一个 while 死循环,方便父子进程的进程控制块进行查看。
$ cat /proc/32261/limits
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 1024 31683 processes
Max open files 1024 4096 files
Max locked memory 65536 65536 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 31683 31683 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us
通过 cat /proc/32261/limits 命令我们可以看到系统对这个用户的一些资源限制,包括 CPU 时间,最大文件大小,最大栈大小,进程数,文件数,最大地址空间等等的资源。
4 总结
经过这次对 Linux 系统的 fork 系统调用的分析,主要有以下几点总结:
fork 调用是 Linux 系统中很重要的一个创建进程的方式,系统级别的进程和线程都是通过 fork 系统调用来实现的,它的实现其实也依靠了 clone 系统调用;
在 Linux 系统中,一个进程内多个线程其实就是共享了父进程大部分资源的子进程,内核通过 clone_flags 来控制创建这种特别的进程;
Linux 其实也是一个软件,但是它是一个复杂无比的软件。虽然从源码来说,不同的部分分得挺清楚,但是具体到一个个函数的执行,对于我们新手而言,如果没有注释,有时候真的很难知道一个函数的参数是什么意思。这时候就要依靠搜索引擎的力量了。