linux内核调度 0号进程,Linux内核源代码情景分析---第四章 进程与进程调度

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结构,而在结构的上面就用作进程的系统空间堆栈、

a4c26d1e5885305701be709a3d33442f.png

a4c26d1e5885305701be709a3d33442f.png

数据结构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()的叙述。

a4c26d1e5885305701be709a3d33442f.png

【三个静态队列描述 第一个组织第二个组织第三个组织】

这个组织虽然确定了每个进程的"宗族"关系,涵盖了系统中所有的进程,但是,要在这个组织中根据进程号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], &regs, 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, &regs, 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], &regs, 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,

&current->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;

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/534580.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redhat linux 系统管理,红帽系统管理一 (RH124)

红帽系统管理一 (RH124)时间&#xff1a;2018-04-24 14:06:52第 1 部分 — 红帽企业 Linux 管理员所需掌握的核心系统管理任务本课程涉及使用红帽 企业 Linux 7。红帽系统管理一 (RH124) 专为之前没有 Linux 系统管理经验的 IT 专业人员设计。本课程侧重讲解 Linux 系统的核心管…

linux命令行聊天,Linux 下使用talk 进行聊天

Linux中talk命令参数程序用于Internet上两个用户之间进行“交谈”&#xff1a;通过键盘输入“说话”&#xff0c;通过看终端屏幕“聆听”。Linux中talk命令参数程序的使用很简单&#xff0c;只要知道交谈对象的地址&#xff0c;就可以邀请对方交谈。格式&#xff1a;talk usert…

在linux中which命令,Linux 中 which 命令怎么用?

在Linux中which命令的作用是在PATH变量指定的路径中&#xff0c;搜索某个系统命令的位置&#xff0c;并且返回第一个搜索结果&#xff0c;其用法为“which [文件...]”&#xff0c;其参数有“-n”、“-p”、“-w”和“-V”。Linux which命令用于查找文件。which指令会在环境变量…

c语言怎样用格式化文件存储,如何用格式化的方式读写文件

对格式会来说&#xff0c;C语言的格式读写文件是很有要求的&#xff0c;在前面我们已经讲解了如何去进行字符的输入输出&#xff0c;但事实真相&#xff0c;数据的类型是很丰富的&#xff0c;而且大家已经熟悉了用printf和scanf函数进行格式化的输入输出&#xff0c;他们是向终…

linux下qq怎么截图,ubuntu 12.04使用QQ截图安装教程

相信用过linux系统的朋友都知道&#xff0c;linux下的截图软件是在不咋的。虽然系统本身有带截图工具&#xff0c;但是却苦于没有办法在截下来的图片上作画圈、写文字说明等动作。应该有不少朋友也是从windows系统下转到linux下做开发的&#xff0c;不知道大家对QQ截图这个软件…

android 资源如何下沉,关于Android业务模块下沉的一些实践及总结

此文已由作者徐铭阳授权网易云社区发布。欢迎访问前言最近在做需求过程中&#xff0c;一些类似学校选择、城市选择等业务相关模块想单独抽离出来&#xff0c;遇到一些诸如模块管理、通信方面的问题来背景最近有一个需求是学校列表&#xff0c;没错&#xff0c;就是我们平时总见…

android sqlite存储数据,Android之SQLite数据存储

关于SQLite的出生长大和壮大&#xff0c;这里就略去了&#xff0c;只记几点比较重要的用法&#xff1a;SQLite所支持的数据类型&#xff1a;SQLite&#xff0c;SQLite3支持 NULL、INTEGER、REAL(浮点数字)、TEXT(字符串文本)和BLOB(二进制对象)数据类型&#xff0c;虽然它支持的…

android studio 跨进程,Android IPC机制(三)在Android Studio中使用AIDL实现跨进程方法调用...

本文首发于微信公众号「后厂技术官」在上一篇文章Android IPC机制(二)用Messenger进行进程间通信中我们介绍了使用Messenger来进行进程间通信的方法&#xff0c;但是我们能发现Messenger是以串行的方式来处理客户端发来的信息&#xff0c;如果有大量的消息发到服务端&#xff0…

nubia ui 5.0 android,流畅度爆棚 搭Android 5.0系统新机一览

近期各品牌新机都不少&#xff0c;而且90%以上都是Android系统的手机&#xff0c;可见安卓手机的主导地位仍在上升。而在系统层次&#xff0c;Android 5.0已经逐步开始普及&#xff0c;近期上市新机百分百均采用了这一系统&#xff0c;值得一提的是定制不再“深度”&#xff0c…

html 百分比正方形,css实现未知宽度的正方形需求

今天群里有哥们问了一下&#xff0c;百分比宽度的正方形如何用css实现。其实就是不定宽的正方形如何用css实现。第一个方法利用图片的等比例缩放&#xff0c;用base64写一个1*1的透明png图片&#xff0c;宽度100%&#xff0c;这样容器就自动被撑成一个正方形&#xff0c;demo如…

html引用本地图片不能是桌面的,Img标签与本地文件:/// URL不显示在Microsoft Edge Web浏览器...

在我的桌面应用程序中&#xff0c;我创建了一个临时HTML文件(旨在让用户打印报告)&#xff0c;然后通过默认显示网页浏览器。这个HTML文件保存在一个临时文件夹&#xff0c;例如&#xff1a;C:/Users/UserName/AppData/Local/TempImg标签与本地文件&#xff1a;/// URL不显示在…

怎么用计算机算立方数,计算器的使用方法

计算器人们都很熟悉&#xff0c;尤其是从事数据行业的人更要懂得如何使用计算器&#xff0c;那么外行人士想要正确使用计算器该怎么办呢?今天小编就来为您解围。计算器包括标准型和科学型两种&#xff0c;其中标准型使用方法如下&#xff1a;1、键入数字时&#xff0c;按下相应…

麻省理工学院计算机科学与工程博士,2020年麻省理工学院博士读几年

麻省理工学院(Massachusetts Institute of Technology)&#xff0c;简称麻省理工(MIT)&#xff0c;坐落于美国马萨诸塞州剑桥市(大波士顿地区) &#xff0c;是世界著名私立研究型大学、被誉为"世界理工大学之最"。麻省理工学院博士读几年麻省理工学院博士一般读5年&a…

为什么计算机语言都是英语,编程为什么都是英语 编程为什么没有汉语

编程为什么都是英语&#xff1f;编程为什么没有汉语&#xff1f;一些想写编程但英语又不好的同学想知道编程为什么都是英语&#xff0c;为什么没有汉语&#xff0c;下面就让小编为大家介绍一下其实是有汉语编程语言的&#xff0c;感兴趣的小伙伴一起来看看吧&#xff0c;有一门…

学计算机随随便便上万,大学“最烧钱”专业排行榜,“家境一般”慎入,“土豪”请随意...

俗话说“高考七分考&#xff0c;三分报”&#xff0c;而这也意味着在高考当中不仅考试重要&#xff0c;在高考后的填报志愿也是非常重要的&#xff0c;因为填报志愿也代表着在大学期间学习什么专业&#xff0c;也代表着在毕业后会从事什么行业&#xff0c;所以报考专业一定要慎…

苏大计算机学院在哪,苏州大学和扬州大学都位于江苏,这两所大学,哪一所的实力更强?...

原标题&#xff1a;苏州大学和扬州大学都位于江苏&#xff0c;这两所大学&#xff0c;哪一所的实力更强&#xff1f;一个朋友问我这么一个问题&#xff1a;“苏州大学和扬州大学都位于江苏&#xff0c;这两所大学&#xff0c;哪一所的实力更强&#xff1f;”经常看到这么一句话…

计算机技术博客博客知乎,我的技术博客的选择:CSDN、博客园、简书、知乎专栏仍是Github Page?...

有不少技术人员在学习到必定程度后发现了写博客的重要性&#xff0c;一方面帮助本身记忆&#xff0c;一方面也能帮助他人解决问题&#xff0c;因而会选择本身开始写博客&#xff0c;以后又发现平台太多不知从何下手&#xff0c;在这里我根据本身写博客的经验比较一下各个平台的…

计算机文化基础重点知识归纳,计算机文化基础_第二章重点知识总结(考试必备!!!)...

操作系统&#xff1a;是管理软硬件资源、控制程序执行、改善人机界面、合理组织计算机工作流程和为用户使用计算机提供良好运行环境的一种系统软件。操作系统的四种特性&#xff1a;并发性、共享性、虚拟性、异步性。并发性&#xff1a;是指两个或两个以上的运行程序在同一时间…

c++生成光栅条纹程序_共享屋:一文让你认识光栅尺和编码器

坚持学习与健身&#xff0c;是对自己最大的投资每天进步一点点共享屋数控机床电气调试与维修微信&#xff1a;799309212大家好&#xff0c;我是共享屋&#xff0c;每天通过一篇文章分享我的经验与观察&#xff0c;希望能够给你一些启发或者帮助。分享的主题是有关数控机床电气调…

修改蓝牙耳机按键映射_喜欢玩游戏的不要错过了,五款高性能游戏蓝牙耳机推荐...

随着电竞行业的发展&#xff0c;不少人都会在休闲时打打游戏娱乐一下&#xff0c;尤其是手游火爆以后&#xff0c;玩游戏的人就更多了&#xff0c;像是王者荣耀&#xff0c;和平精英&#xff0c;穿越火线等等&#xff0c;几乎都是装机必备。不过现在的游戏需要有很好的声音呈现…