4.1 进程四要素
什么是进程?
1:有一段代码段供其执行,这代码段不一定是进程所专用,可以与其他进程公用。
2:每个进程有其专用的系统空间的堆栈(栈)【这个栈是进程起码的“私有财产”】
3:在内核中,要有task_struct 进程控制块【task_struct
进程控制块就是像是进程的财产登记卡,记录着进程所拥有的各项资源,只有有了task_struct,进程才能被内核所调度】
4:拥有专有的用户空间【各进程的用户空间是相互独立的,但是各进程共享系统空间,且各进程不能直接(不通过系统调用)改变系统空间的内容】
如果有 1 2 3,完全没有用户空间 ==>
内核线程(kernel thread,比如kswapd)
如果有 1 2 3,没有独立的用户空间 ==>因为有用户空间但不是独立的,所以称为用户线程
linux 系统中,进程(process)和任务(task)是同一个意思;
Unix系统的进程在Intel 的技术资料中则称为“任务”;linux源自Unix和i386系统结构;
linux系统运行时的第一个进程是在系统初始化阶段“捏造”出来的,而此后的进程或线程则都是由一个已存在的进程像细胞分裂那样通过系统调用复制出来的,称为fork
或 clone
Intel的i386 通过任务门 和
进程的TSS段(任务状态段,TSS位于GDT中,包含了该进程的关键的状态信息【控制信息】,但linux却没有使用任务门)来在硬件上实现任务的切换;【但其实因为该处理器的CISC架构以及这种切换方式,不是特别的效率高的,其实任务切换可以做的更加的简单,i386的这个切换可以理解为一种“高级语言”,而我们在做操作系统时,往往使用效率更高的“低级语言
汇编等”】;
i386 CPU 要求软件去设置TR 与 TSS,TR指向CPU当前正在执行的任务进程的TSS,Intel的设计意图是
随着任务的切换而走马灯似的设置TR的内容;
CPU因中断或系统调用从用户空间进入系统空间时,会由于运行级别的变换而导致自动更换栈,不同的栈指针来自于当前任务的TSS中包含的栈指针(SS
ESP);因为Linux系统中只用到了两个运行级别,即0级与3级,所以对于内核来说,TSS中只剩下0级的堆栈指针 即SS0
ESP0
Linux
系统在任务的切换过程中,因为效率的考虑,并不根据任务的切换去设置TR,而是直接修改TSS(Linux内核只使用这样一个TSS,用来保存当前任务的状态)中的SS0
ESP0==》铁打的营盘流水的兵,就一个TSS,就像一座营盘,建立后就不再动了,而里面的内容,也就是当前任务的系统堆栈指针,则随着进程的调度切换而流水似地变动。这是因为改变TSS中的SS0
ESP0所花的开销比通过装入TR以更换一个TSS要小得多。
Linux中TSS不是某个进程所独占的,他而是全局性的公共资源。内核中虽然有多个TSS,但是每个CPU就只有(使用)一个TSS,一经装入就不再变了。
Unix
Linux系统中任务的切换只发生在系统空间中,这点很好理解,因为共享的系统空间中拥有各个进程的各种资源;
每个进程都有一个task_struct数据结构和一片用作系统空间栈的存储空间。内核在为每个进程分配一个task_struct结构时,实际上分配两个连续的物理页面(共8192个字节),这两个页面的底部用作进程的task_struct结构,而在结构的上面就用作进程的系统空间堆栈、
数据结构task_struct的大小约为1K字节,所以进程的系统空间的堆栈的大小约为7K字节,注意:系统空间堆栈的空间不像用户空间堆栈那样可以在运行时动态的扩展,如第2章所述,而是静态的确定了的,所以,在中断服务程序中、内核软中断服务程序以及其他设备驱动程序的设计中,应注意不能让这些函数嵌套太深【避免嵌套太深,导致栈的溢出】,同时在这些函数中也不适宜使用太多、太大的局部变量。
一个进程必定又是一个内核线程(内核线程的要求是
1.有代码段 2.有专用的系统栈
3.有task_struct数据接否);
内核中有一个宏操作current,它指向当前进程task_struct结构的指针。
接下来,可具体分析下task_struct结构,
struct task_struct
{
volatile long state;
unsigned
long flags;
int
sigpending;
mm_segment_t
addr_limit;
struct
exec_domain *exec_domain;==>除personality外,应用程序还有一些其他的版本间的差异,从而形成了不同的“执行域”,这个指针就是指向描述本进程所属的执行域的数据结构
volatile
long need_resched;
unsigned long
ptrace;
int lock_depth;
long
counter;
long nice;
unsigned
long policy;==>适用于本进程的调度政策,详见进程的调度与切换
struct mm_struct
*mm;
int has_cpu,
processor;
unsigned long
cpus_allowed;
struct list_head
run_list;
unsigned long
sleep_time;
struct task_struct *next_task,
*prev_task;
struct mm_struct
*active_mm;
struct
linux_binfmt *binfmt;
int
exit_code, exit_signal;
int
pdeath_signal;==>这三个详见系统调用exit()与wait4()
unsigned
long personality;
int dumpable:1;
int did_exec:1;
pid_t
pid;==>进程号
pid_t
pgrp;
pid_t
tty_old_pgrp;
pid_tpgrp;
pid_t tgid;
int
leader;
//pgrppgrpleader
当一个用户登陆到系统时,就开始一个进程组(session),此后创建的进程都属于这同一个session。此外,若干进程可以通过“管道”组合在一起,如
ls | wc -l,从而形成进程组,详见“系统调用exec”一节
struct task_struct *p_opptr,
*p_pptr, *p_cptr, *p_ysptr, *p_osptr;
struct list_head
thread_group;
struct task_struct
*pidhash_next;
struct task_struct
**pidhash_pprev;
wait_queue_head_t
wait_chldexit;
struct semaphore
*vfork_sem;
unsigned long rt_priority;==>优先级别以及“实时”优先级别,详见进程的调度与切换
unsigned long it_real_value,
it_prof_value, it_virt_value;
unsigned long it_real_incr,
it_prof_incr, it_virt_incr;
struct timer_list
real_timer;
struct tms times;
unsigned long
start_time;
long per_cpu_utime[NR_CPUS],
per_cpu_stime[NR_CPUS];
unsigned long min_flt, maj_flt,
nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1;
uid_t
uid,euid,suid,fsuid;
gid_t
gid,egid,sgid,fsgid;==>这8个主要与文件操作权限有关,见文件系统一章
int ngroups;
gid_t
groups[NGROUPS];
kernel_cap_t
cap_effective, cap_inheritable, cap_permitted;
==>一般进程都不能"为所欲为",而是各自被赋予了
各种不同的权限。例如,一个进程是否可以通过系统调用ptrace()跟踪另一个进程,就是由该进程是否具有CAP_SYS_PTRACE授权决定的;一个进程是否有权重新引导操作系统,则取决于该进程是否具有CAP_SYS_BOOT授权。这样,就把进程的各种权限分细了,而不再是笼统地取决于一个进程是否是"特权用户"进程。每一种权限都由一个标志位代表,内核中提供了一个inline函数capable(),用来检验当前进程是否具有某种权限。如capable(CAP_SYS_BOOT)就是检查当前进程是否有权重引导操作系统〔返回非0表示有权)。值得注意的是,对操作权限的这种划分与文件访问权限结合在一起,形成了系统安全性的基础。在现今的网络时代,这种安全性正在变得愈来愈重要,而这方面的研究与发展也是一个重要的课题。
int
keep_capabilities:1;
struct
user_struct *user;==>指向一个user_struct结构,该数据结构代表着进程所属的用户。注意这跟Unix内核中每个进程的user结构时两码事。Linux内核中user结构是非常简单的,详见“系统调用fork()”一节。
struct
rlimit rlim[RLIM_NLIMITS];==>这是一个结构数组,表明进程对各种资源的使用数量所受的限制,
struct rlimit {
unsigned long rlim_cur;
unsigned long rlim_max;
};
unsigned short
used_math;
char comm[16];
int link_count;
struct tty_struct
*tty;
unsigned int
locks;
struct sem_undo
*semundo;
struct sem_queue
*semsleeping;
struct thread_struct
thread;
struct fs_struct
*fs;
struct files_struct
*files;
spinlock_t
sigmask_lock;
struct signal_struct
*sig;
sigset_t blocked;
struct sigpending
pending;
unsigned long
sas_ss_sp;
size_t
sas_ss_size;
int (*notifier)(void
*priv);
void
*notifier_data;
sigset_t
*notifier_mask;
u32
parent_exec_id;
u32
self_exec_id;
//parent_exec_idself_exec_id
==>与进程组session有关,详见系统调用exit()与wait4()
spinlock_t
alloc_lock;
}
主要可分为状态、性质、资源、和组织等几大类;
volatile long state;表示进程当前运行的状态
#define TASK_RUNNING 0==>进程处于就绪态(),而不是表达该进程就是当前正在运行的进程;当进程处于这个状态时,内核就将该进程的task_struct结构通过其队列头run_list挂入一个“运行队列”
#define
TASK_INTERRUPTIBLE 1==>进程处于睡眠状态,因信号的到来而被唤醒。interruptible_sleep_on()
和 wake_up_interruptible()用于浅度睡眠;
#define TASK_UNINTERRUPTIBLE
2==>进程处于深度睡眠状态,不受信号(signal,也称软中断)的打扰;sleep_on()
和wake_up()用于深度睡眠;深度睡眠一般只用于临界区和关键性的部位,而“可中断”的睡眠那就是通用的,特别当进程在“阻塞性”的系统调用中等待某一事件的发生时,就不应该进入深度睡眠,否则就不能对别的事件作出反应,别的进程就不能通过发一个信号来杀掉这个进程;这里的INTERRUPTIBLE与UNINTERRUPTIBLE与”中断“毫无关系,而是说睡眠能否因其他事件而中断即唤醒,不过其他事件主要指”信号“,而信号的概率实际上与中断的概率是相同的,所以这里所谓的INTERRUPTIBLE也是指这种”软中断“。
#define TASK_ZOMBIE 4==>进程已经去世(exit),但是其户口尚未注销
#define TASK_STOPPED 8==>进程处于就绪态(),主要用于调试目的,进程接收到一个SIGSTOP信号后就将运行状态改为TASK_STOPPED而进入挂起状态,然后在接收到一个SIGCONT信号后,又恢复继续运行。
unsigned
long flags;;反映进程状态的信息,但不是运行状态,而是与管理有关的其他信息 ,见下面的注释
#define PF_ALIGNWARN
0x00000001
#define PF_STARTING
0x00000002
#define PF_EXITING
0x00000004
#define PF_FORKNOEXEC
0x00000040
#define PF_SUPERPRIV
0x00000100
#define PF_DUMPCORE
0x00000200
#define PF_SIGNALED
0x00000400
#define PF_MEMALLOC
0x00000800
#define PF_VFORK
0x00001000
#define PF_USEDFPU
0x00100000
再看下task_struct除上述外的其他的一下状态信息变量:
int
sigpending==>表示进程收到了信号,但尚未处理,详见进程间通信中的信号一节
mm_segment_t addr_limit==>虚拟地址空间的上限。对进程而言是其用户空间的上限,所以是0XBFFFFFFF,对内核线程而言则
是系统空间的上限,所以是0XFFFFFFFF。
volatile long
need_resched==>与调度有关,表示CPU从系统空间返回用户空间前夕要进行一次调度。
long counter==>与调度有关,详见进程的调度与切换一节
unsigned long personality==>由于Unix有许多不同的版本和变种,应用程序也就有了适用的范围,所以根据执行程序的不
同,每个进程都有其个性。
其他的直接看上面的结构里面的注释
最后,每一个进程都不是孤立地存在于系统中,而总是根据不同的目的、关系和需要与其它的进程相联系。从内核的角度看,则是要按不同的目的和性质将每个进程纳入不同的组织中。第一个组织是由每个进程的"家庭与社会关系"形成的"宗族"或"家谱"。这是一种树型的组织,通过指针p_opptr、p_pptr、p_cptr、p_ysptr和p_osptr构成。其中p_opptr和p_pptr指向父进程的task_struct结构,p_cptr指向最"年轻"的子进程,而p_ysptr和p_osptr则分别指向其"弟弟"和"哥哥",从而形成一个子进程链。这些指针确定了一个进程在其"宗族"中的上、下、左、右关系,详见本章中对fork()和exit()的叙述。
【三个静态队列描述 第一个组织第二个组织第三个组织】
这个组织虽然确定了每个进程的"宗族"关系,涵盖了系统中所有的进程,但是,要在这个组织中根据进程号pid找到一个进程却非易事。进程号的分配是相当随机的,在进程号中并不包含任何可以用来找到一个进程的路径信息,而给定一个进程号要求找到该进程的task_struct结构却又是常常要用到的一种操作。于是,就有了第二个组织,那就是一个以杂凑表为基础的进程队列的阵列。当给定一个pid要找到该进程时,先对pid施行杂凑计算,以计算的结果为下标在杂凑表中找到一个队列,再顺着该队列就可以较容易地找到特定的进程了。杂凑表pidhash是在kernel/fork.c中定义的:struct
task_struct *pidhash[PIDHASH_SZ];
杂凑表的大小PIDHASH_SZ为1024。由于每个指针的大小是4个字节,所以整个杂凑表(不包括各个队列)正好占一个页面。每个进程的task_struct数据结构都通过其pidhash_next和pidhash_pprev两个指针放入
到杂凑表中的某个队列中,同一队列中所有进程的pid都具有相同的杂凑值。由于杂凑表的使用,要找到pid为某个给定值的进程就很迅速了。
当内核需要对每一个进程做点什么事情时,还需要将系统中所有的进程都组织成一个线性的队列,这样就可以通过一个简单的for循环或while循环遍历所有进程的task_struct结构。所以,第三个组织就是这么一个线性队列。系统中第一个建立的进程为init_task,这个进程就是所有进程的总根,所以这个线性队列就是以init_task为起点(也可以把它看成是一个队列头〕,后继每创建一个进程就通过其init_task结构中的next_task和prev_task两个指针链入这个线性队列中。
每个进程都必然同时身处这三个队列之中,直到进程消亡时才从这三个队列中摘除,所以这三个队列都是静态的。
在运行的过程中,一个进程还可以动态地链接进"可执行队列"接受系统的调度。实际上,这是最重要的队列,一个进程只有在可执行队列中才有可能受到调度而投入运行。与前几个队列不同的是,
一个进程的task_struct是通过其list_head数据结构run_list、而不是个别的指针,链接进可执行队列的。以前说过,这是用于双向链接的通用数据结构,具有一些与之配套的函数或宏操作,处理的效率比较高,也使代码得以简化。可执行队列的变化是非常频繁的,一个进程进入睡眠时就从队列中脱链,被唤醒时则又链入到该队列中,在调度的过程中也有可能会改变一个进程在此队列中的位置。详见本章"进程调度与进程切换"以及"系统调用nanosleep()"中的有关叙述。
4.2 进程三部曲:创建、执行与消亡
就像世上万物都有产生、发展与消亡的过程一样,每个进程也有被创建、执行某段程序以及最后消亡的过程。
在linux系统中,第一个进程是系统固有的、与生倶来的或者说是由内核的设计者安排好了的(系统中第一个建立的进程为init_task,这个进程就是所有进程的总根)。内核在引导并完成了基本的初始化以后,就有了系统的第一进程(实际上是内核线程)。除此之外,所有其它的进程和内核线程都由这个原始进程或其子孙进程所创建,都是这个原始进程的"后代"。在linux系统中,一个新的进程一定要由一个已经存在的进程"复制"出来,而不是"创造"出来(而所谓"创建"实际就是复制)。所以,linux系统(unix也一样)并不向用户(即进程)提供类似这样的系统调用:
int creat_pro(int (*fn)(void
*), void *arg, unsigned long options);
可是在很多操作系统(包括一些unix的变种)中都采用了
"一揽子"的方法。它"创造"出一个进程,并使该进程从函数指针数指针fn所指的地方开始执行。根据不同的情况和设计,参数fn也可以换成一个可执行程序的文名。这里所谓"创造",包括为进程分配所需的资源、包括属于最低限度的task_struct
数据结构和系统空间堆栈,并初始化这些资源;还要设置其系统空间堆栈,使得这个新进程看起来就好像是一个本来就已经存在而正在睡眠的进程。当这个进程被调度运行的时候,其"返回地址",也就是"恢复"运行时的下一条指令,则就在fn指的地方(uc/os就是这样的)。这个"子进程"生下来时两手空空,却可以完全独立,并不与其父进程共享资源。
但是,linux系统(unix也一样)采用的方法却不同。
linux将进程的创建与目标程序的执行分成两步:
第一步是从已经存在的"父进程"中像细胞分裂一样地复制出一个"子进程"。这里所谓像"细胞分裂一样",只是打个比方,实际上,复制出来的子进程有自已的task_struct 结构和系统空间堆栈,但与父进程共享其它所有的资源。例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。所以,这一步所做的是"复制"。linux为此提供了两个系统调用,一个是fork(),另一个是clone()。两者的区别在于fork()是全部复制,父进程所有的资源全都通过数据结构的复制"遗传"给子进程。而clone()则可以将资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享。在极端的情况下,一个进程可以clone()出一个线程。所以,系统调用fork()是无参数的,而clone()则带有参数。读者也许已经意识到,fork()其实比clone()更接近本来意义上的"克隆"。确实是这样,原因在于fork()在unix初期即已存在,那时候"克隆"这个词还不像现在这么流行,而既然业已存在,就不宜更改了。否则,也许应该互换一下名字。后来,又增设了
一个系统调用vfork(),也不带参数,但是除task_struct结构和系统空间堆栈以外的资源全都通过数据结构指针的复制"遗传",所以vfork()出来的是线程而不是进程。读者将会看到,vfork()主要是出于效率的考虑而设计并提供的。
第二步是目标程序的执行。一般来说,创建一个新的进程是因为有不同的目标程序要让新的程序去执行(但也不一定),所以,复制完成以后,子进程通常要与父进程分道扬镳,走自己的路。为此提供了一个系统调用execve(),让一个进程执行以文件形式存在的一个可执行程序的映象。
读者也许要问:这两种方案到底哪一种好?应该说是各有利弊。但是更应该说,Linux从unix继承下来的这种分两步走,并且在第一步中采取复制方式的方案,利远大于弊。从效率的角度看,分两步走很有好处。所谓复制,只是进程的基本资源的复制,如task_struct数据结构、系统空间堆栈、页面表等等,对父进程的代码及全局变量则并不需要复制,而只是通过只读访问的形式实现共享,仅在需要写的时候才通过copy_on_write的手段为所涉及的页面建立一个新的副本。所以,总的来说复制的代价是很低的,但是通过复制而继承下来的资源则往往对子进程很有用。读者以后会看到,在计算机网络的实现中,以及在client/server系统中的server—方的实现中,fork()或clone()常常是最自然、最有效、最适宜的手段。更重要的好处是,这样有利于父、子进程间通过pipe来建立起一种简单有效的进程间通信管道,并且从而产生了操作系统的用户界面即shell的"管道"机制。这一点,对于unix的发展和推广应用,对于unix程序设计环境的形成,对于unix程序设计风格的形成,都有着非常深远的影响。可以说,这是一项天才的发明,它在很大程度上改变了操作系统的发展方向。
当然,从另一角度,也就是从程序设计界面的角度来看,则"一揽子"的方案更为简洁。不过fork()加execve()的方案也并不复杂很多。进一步说,这也像练武或演戏一样有个固定的"招式",一旦掌握了以后就不觉得复杂,也很少变化了。再说,如果有必要也可以通过程序库提供一个"一揽子"的库函数,将这两步包装在一起。
创建了子进程以后,父进程有三个选择:
第一是继续走自己的路,与子进程分道扬镳。只是如果子进程先于父进程"去世",则由内核给父进程发一个报丧的信号。
第二是停下来,也就是进入睡眠状态,等待子进程完成其使命而最终去世,然后父进程再继续运行。Linux为此提供了两个系统调用,
wait4()和wait3()。两个系统调用基本相同,wait4()等待某个特定的子进程去世,而wait3()则等待任何一个子进程去世。
第三个选择是"自行退出历史舞台",结束自己的生命。为此设置了一个系统调用exit()。这里的第三个选择其实不过是第一个选择的一种特例,
所以从本质上说是两种选择:一种是父进程不受阻的(non_blocking)方式,也称为"异步"的方式;另一种是父进程受阻的(blocking)方式,或者称为"同步"的方式。
4.3 系统调用fork()
vfork() clone()
fork()与clone()的区别:
pid_t fork(void);
int clone(int (*fn)(void *arg),
void *child_stack, int flags, void *arg);
系统调用__clone()的主要用途是创建一个线程,这个线程可以是内核线程,也可以是用户线程。
创建用户空间线程时,可以给定子线程用户空间堆栈的位置,还可以指定子进程运行的起点。
__clone()也可以创建进程,有选择地复制父进程的资源。而fork()则是全面地复制。还有一个系统调用vfork()其作用也是创建一个线程,但主要只是作为创建进程的中间步骤,目的在于提高创建时的效率,减少系统开销,其程序设计接口则与fork相同。
asmlinkage int sys_fork(unsigned long r4, unsigned long
r5,unsigned long r6, unsigned long r7,
struct
pt_regs regs)
{
return do_fork(SIGCHLD, regs.regs[15], ®s, 0);
}
asmlinkage int sys_clone(unsigned long clone_flags, unsigned
long newsp,
unsigned long r6, unsigned long r7,struct pt_regs
regs)
{
if (!newsp)
newsp = regs.regs[15];
return do_fork(clone_flags, newsp, ®s, 0);
}
asmlinkage int sys_vfork(unsigned long r4, unsigned long r5,
unsigned long r6, unsigned long r7,
struct pt_regs regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD,
regs.regs[15], ®s, 0);
}
这三个系统调用 都是 通过调用
do_fork()来完成的,do_fork()通过不同的参数
在函数体内实现相关资源的拷贝;
下面来解读下这个函数
int do_fork(unsigned long clone_flags, unsigned long
stack_start,struct pt_regs *regs, unsigned
long stack_size)
{
======================函数内解释部分===================================
参数clone_flags由两部分组成,其最低的字节为信号类型,用以规定子进程去世时应该向父进程发出的信号。我们已经看到,对于fork()和vfork()这个信号就是SIGCHILD,而对__clone()则该位段可由调用者决定。第二部分是一些表示资源和特性的标志位(通过这些标志位来采取相应的拷贝动作),对于fork(),这一部分为全0,表对有关的资源都要复制而不是通过指针共享[位为0表示要复制
位为1表示父子先共享]。而对vfork(),则为(CLONE_VFORK
|CLONE_VM),表示父、子进程共用(用户)虚存区间,并且当子进程释放其虚存区间时要唤醒父进程,至于__clone(),则这一部分完全由调用者设定而作为参数传递下来。其中标志位CLONE_PID有特殊的作用,当这个标志位为1时,父、子进程〔线程)共用同一个进程号,也就是说,子进程虽然有其自己的task_structt数据结构,却使用父进程的pid。但是,只有0号进程,也就是系统中的原始进程(实际上是线程),才允许这样来调用__clone(),所以564行对此加以检查。
接着,通过alloc_task_struct()为子进程分配两个连续的物理页面,低端用作子进程的task_struct结构,高端则用作其系统空间堆栈。
注意574行的赋值为整个数据结构的赋值。这样,父进程的整个task_struct就被复制到了子进程的数据结构中。经编译以后,这样的赋值是用memcpy()实现的,所以效率很高。
#define CSIGNAL 0x000000ff
#define CLONE_VM 0x00000100
#define CLONE_FS 0x00000200
#define CLONE_FILES 0x00000400
#define CLONE_SIGHAND 0x00000800
#define CLONE_PID 0x00001000
#define CLONE_PTRACE 0x00002000
#define CLONE_VFORK 0x00004000
#define CLONE_PARENT 0x00008000
#define CLONE_THREAD 0x00010000
#define CLONE_SIGNAL (CLONE_SIGHAND |
CLONE_THREAD)
============================================================
int retval =
-ENOMEM;
struct task_struct
*p;
DECLARE_MUTEX_LOCKED(sem);
if (clone_flags &
CLONE_PID) {
if
(current->pid)
return -EPERM;
}
current->vfork_sem =
&sem;
p =
alloc_task_struct();
if (!p)
goto fork_out;
*p = *current;
retval = -EAGAIN;
if
(atomic_read(&p->user->processes) >=
p->rlim[RLIMIT_NPROC].rlim_cur)
goto
bad_fork_free;
atomic_inc(&p->user->__count);
atomic_inc(&p->user->processes);
if (nr_threads >=
max_threads)
goto
bad_fork_cleanup_count;
get_exec_domain(p->exec_domain);
if (p->binfmt &&
p->binfmt->module)
__MOD_INC_USE_COUNT(p->binfmt->module);
p->did_exec =
0;
p->swappable =
0;
p->state =
TASK_UNINTERRUPTIBLE;
copy_flags(clone_flags,
p);
p->pid =
get_pid(clone_flags);
======================函数内解释部分===================================
task_struct结构中有个指针user,用来指向一个user_struct结构。
一个用户常常有许多个进程,所以有关用户的一些信息并不专属于某一个进程。这样,属于同一用户的进程就可以通过指针user共享这些信息。
显然,每个用户有且只有一个user_struct结构。结构中有个计数器__count对属于该用户的进程数量计数。
可想而知,内核线程并不属于某个用户,所以其task_struct中的user指引为0。
#define UIDHASH_BITS 8
#define UIDHASH_SZ
(1 << UIDHASH_BITS)
static struct
user_struct *uidhash_table[UIDHASH_SZ];
这是一个杂凑(hash)表。对用户名施以杂凑运算,就可以计算出一个下标而找到该用户的user_struct结构。各进程的task_struct结构中还有个数组rlim,对该进程占用各种资源的数量作出限制,而rlim[RLIMIT_NPROC]就规定了该进程所属的用户可以拥有的进程数量。所以,如果当前进程是一个用户进程,并且该用户拥有的进程数量已经达到了规定的限制值,就再不允许它fork()了.那么,对于不属于任何用户的内核线程怎么办呢?
587行中的两个计数器就是为进程的总量而设的。
一个进程除了属于某一个用户之外,还属于某个"执行域"。总的来说,Linux是Unix的一个变种,并且符合POSIX的规定。但是,有很多版本的操作系统同样是Unix变种,同样符合POSIX规定,互相之间在实现细节上却仍然有明显的不同。这就形成了不同的执行域。如果一个进程所执行的程序是为Solaris开发的,那么这个进程就属于Solaris执行域PER_SOLARIS。当然,在Linux上运行的绝大多数程序都属于Linux执行域。在task_struct结构中有一个指针exec_domain,可以指向一个exec_domain数据结构。
常数PID_MAX定义为0X8000。可见,进程号的最大值是0X7FFF
即32767。进程号0?299是为系统进程(包括内核线程)保留的,主要用于各种"保护神"进程。以上这段代码的逻辑并不复杂,我们就不多加解释了。
====================================================================
p->run_list.next =
NULL;
p->run_list.prev =
NULL;
if ((clone_flags &
CLONE_VFORK) || !(clone_flags & CLONE_PARENT)) {
p->p_opptr =
current;
if (!(p->ptrace &
PT_PTRACED))
p->p_pptr =
current;
}
p->p_cptr =
NULL;
init_waitqueue_head(&p->wait_chldexit);
p->vfork_sem =
NULL;
spin_lock_init(&p->alloc_lock);
p->sigpending =
0;
init_sigpending(&p->pending);
p->it_real_value =
p->it_virt_value = p->it_prof_value = 0;
p->it_real_incr =
p->it_virt_incr = p->it_prof_incr = 0;
init_timer(&p->real_timer);
p->real_timer.data =
(unsigned long) p;
p->leader = 0;
p->tty_old_pgrp =
0;
p->times.tms_utime =
p->times.tms_stime = 0;
p->times.tms_cutime =
p->times.tms_cstime = 0;
#ifdef CONFIG_SMP
{
int i;
p->has_cpu = 0;
p->processor =
current->processor;
for(i = 0; i < smp_num_cpus;
i++)
p->per_cpu_utime[i] =
p->per_cpu_stime[i] = 0;
spin_lock_init(&p->sigmask_lock);
}
#endif
p->lock_depth =
-1;
p->start_time =
jiffies;
======================函数内解释部分===================================
====================================================================
retval = -ENOMEM;
if (copy_files(clone_flags,
p))
goto
bad_fork_cleanup;
if (copy_fs(clone_flags,
p))
goto
bad_fork_cleanup_files;
if (copy_sighand(clone_flags,
p))
goto
bad_fork_cleanup_fs;
if (copy_mm(clone_flags,
p))
goto
bad_fork_cleanup_sighand;
retval = copy_thread(0,
clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto
bad_fork_cleanup_sighand;
p->semundo =
NULL;
p->parent_exec_id =
p->self_exec_id;
p->swappable =
1;
p->exit_signal = clone_flags
& CSIGNAL;
p->pdeath_signal =
0;
p->counter =
(current->counter + 1) >> 1;
current->counter >>=
1;
if
(!current->counter)
current->need_resched =
1;
retval =
p->pid;
p->tgid =
retval;
INIT_LIST_HEAD(&p->thread_group);
write_lock_irq(&tasklist_lock);
if (clone_flags &
CLONE_THREAD) {
p->tgid =
current->tgid;
list_add(&p->thread_group,
¤t->thread_group);
}
SET_LINKS(p);
hash_pid(p);
nr_threads++;
write_unlock_irq(&tasklist_lock);
if (p->ptrace &
PT_PTRACED)
send_sig(SIGSTOP, p,
1);
wake_up_process(p);
++total_forks;
fork_out:
if ((clone_flags &
CLONE_VFORK) && (retval >
0))
down(&sem);
return retval;
bad_fork_cleanup_sighand:
exit_sighand(p);
bad_fork_cleanup_fs:
exit_fs(p);
bad_fork_cleanup_files:
exit_files(p);
bad_fork_cleanup:
put_exec_domain(p->exec_domain);
if (p->binfmt &&
p->binfmt->module)
__MOD_DEC_USE_COUNT(p->binfmt->module);
bad_fork_cleanup_count:
atomic_dec(&p->user->processes);
free_uid(p->user);
bad_fork_free:
free_task_struct(p);
goto fork_out;
}
4.4 系统调用execve()
在大多数情况下,如果复制出来的子进程不能与父进程分道扬镳,走自己的路,那就没有多大的意思,所以执行一个新的可执行程序是进程生命历程中关键性的一步。linux为此提供了一个系统调用execve(),而在C语言的程序库中又在此基础上向应用程序提供一整套的库函数,包括execl()
execlp() execleo() execv() execvp()
系统调用execve()内核入口是sys_execve()。sys_execve()就调用do_execve(),
以完成其主体部分的工作。
显然,先要将给定的可执行程序文件找到并打开,do_execve()就是为此而调用的.
当目标文件已经打开,下一步就要从文件中装入可执行程序了。内核中为可执行程序的装入定义了一个数据结构linux_binprm,这个数据结构将运行一个可执行文件时所需的信息组织在一起。
struct
linux_binprm{
char buf[BINPRM_BUF_SIZE];
struct page *page[MAX_ARG_PAGES];
unsigned long p;
int sh_bang;
struct file * file;
int e_uid, e_gid;
kernel_cap_t cap_inheritable, cap_permitted,
cap_effective;
int argc, envc;
char * filename;
unsigned long loader, exec;
}
int do_execve(char * filename, char ** argv, char ** envp,
struct pt_regs * regs)
{
struct linux_binprm bprm;
struct file *file;
int retval;
int i;
file = open_exec(filename);
retval = PTR_ERR(file);
if (IS_ERR(file))
return retval;
bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
memset(bprm.page, 0,
MAX_ARG_PAGES*sizeof(bprm.page[0]));
bprm.file = file; -->保存打开文件的file结构指针
bprm.filename = filename;
bprm.sh_bang =
0;-->
bprm.loader = 0;
bprm.exec = 0;
if ((bprm.argc = count(argv, bprm.p / sizeof(void *))) < 0)
{
allow_write_access(file);
fput(file);
return bprm.argc;
}
if ((bprm.envc = count(envp, bprm.p / sizeof(void *))) < 0)
{
allow_write_access(file);
fput(file);
return bprm.envc;
}
retval = prepare_binprm(&bprm);
if (retval < 0)
goto out;
retval = copy_strings_kernel(1, &bprm.filename,
&bprm);
if (retval < 0)
goto out;
bprm.exec = bprm.p;
retval = copy_strings(bprm.envc, envp, &bprm);
if (retval < 0)
goto out;
retval = copy_strings(bprm.argc, argv, &bprm);
if (retval < 0)
goto out;
retval = search_binary_handler(&bprm,regs);
if (retval >= 0)
return retval;
out:
allow_write_access(bprm.file);
if (bprm.file)
fput(bprm.file);
for (i = 0 ; i < MAX_ARG_PAGES ; i++) {
struct page * page = bprm.page[i];
if (page)
__free_page(page);
}
return retval;
}