第三章 进程

3.1:进程、轻量级进程和线程

       进程是分配资源的最小单元,而线程是CPU调度的最小单元。

当一个进程创建的时候,他与父进程基本相同。他接受父进程地址空间的拷贝,并且从进程创建的系统调用的下一条代码开始,执行和父进程相同的代码。尽管父子进程共享同样的代码页,但是他们的数据并不相同,子进程有自己独立的堆和栈,因此子进程对内存的修改,父进程是不可见的。

为了支持多线程应用程序(拥有很多相对独立执行流的用户程序共享应用程序的大部分数据结构),一个进程由几个用户线程组成。从内核的角度来看,多线程应用程序只是一个进程,多线程应用程序的多个执行流的创建处理和调度都是在用户态进行的。

但是,如果一个进程对应了两个用户线程,考虑一个线程发生了阻塞式系统调用,那么,本来应该运行的第二个线程也被阻塞。

因此,需要使用轻量级线程来对多线程应用提供更好的支持。轻量级进程基本可以共享一些资源,比如地址空间,打开的文件等。当然,访问共享的资源需要进程同步。不应该把轻量级进程和用户态线程混淆起来。用户态线程是完全由用户级的库函数处理的不同执行流,多线程的程序是作为单独的linux进程来执行的。

有了轻量级进程之后,可以通过将轻量级进程和线程关联,使得每个线程都可以由内核独立的调度(一对一模型)。

             

3.2:进程描述符

3.2.1:进程状态

       进程状态有多种。其中:

       TASK_RUNNING状态,表示进程要么在CPU上执行,要么准备执行(放在等待队列上)。

3.2.2:标识一个进程

       能够被独立调度的每个执行上下文都有自己的进程描述符,因此,轻量级进程也有。一般使用进程描述符的首地址来标识进程非常方便。

另外,也可以使用进程标识符pid来标识进程。系统使用不同的 pid来表示一个进程或者轻量级进程(内核线程)。一般情况下,pid最大为32767,刚好是一页的位数(4*1024*8)。

由于一个进程或者轻量级进程可能对应多个用户级线程(以下简称线程)。因此,将对应同一个进程的线程分成一个组(线程组),这个组中所有的线程和唯一的进程或唯一的轻量级进程使用相同的pid,就是进程的pid,存到这个进程或者轻量级进程的进程描述符(PCB)的tgid中。

进程描述符如下所示:

struct task_struct {

       volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */

       struct thread_info *thread_info; //指向task的thread_info

       atomic_t usage;

       unsigned long flags;     /* per process flags, defined below */

       unsigned long ptrace;

       int lock_depth;             /* Lock depth */

       int prio, static_prio; // prio表示进程的动态优先级,static_prio表示进程静态优先级

       struct list_head run_list;

       prio_array_t *array; // 指向包含进程的运行队列

       unsigned long sleep_avg;

       unsigned long long timestamp, last_ran;

       int activated;

       unsigned long policy;

       cpumask_t cpus_allowed;

       unsigned int time_slice, first_time_slice; // time_slice表示进程还剩余的时钟节拍数

       struct list_head tasks;

       struct list_head ptrace_children;

       struct list_head ptrace_list;

       struct mm_struct *mm, *active_mm;

/* task state */

       struct linux_binfmt *binfmt;

       long exit_state;

       int exit_code, exit_signal;

       int pdeath_signal;  /*  The signal sent when the parent dies  */

       /* ??? */

       unsigned long personality;

       unsigned did_exec:1;

       pid_t pid;

       pid_t tgid;

       struct task_struct *real_parent; /* real parent process (when being debugged) */

       struct task_struct *parent;    /* parent process */

      

       struct list_head children;      /* list of my children */

       struct list_head sibling; /* linkage in my parent's children list */

       struct task_struct *group_leader; /* threadgroup leader */

       /* PID/PID hash table linkage. */

       struct pid pids[PIDTYPE_MAX];

       struct completion *vfork_done;          /* for vfork() */

       int __user *set_child_tid;             /* CLONE_CHILD_SETTID */

       int __user *clear_child_tid;          /* CLONE_CHILD_CLEARTID */

       unsigned long rt_priority;

       unsigned long it_real_value, it_real_incr;

       cputime_t it_virt_value, it_virt_incr;

       cputime_t it_prof_value, it_prof_incr;

       struct timer_list real_timer;

       cputime_t utime, stime;

       unsigned long nvcsw, nivcsw; /* context switch counts */

       struct timespec start_time;

/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */

       unsigned long min_flt, maj_flt;

/* process credentials */

       uid_t uid,euid,suid,fsuid;

       gid_t gid,egid,sgid,fsgid;

       struct group_info *group_info;

       kernel_cap_t   cap_effective, cap_inheritable, cap_permitted;

       unsigned keep_capabilities:1;

       struct user_struct *user;

       int oomkilladj; /* OOM kill score adjustment (bit shift). */

       char comm[TASK_COMM_LEN];

/* file system info */

       int link_count, total_link_count;

/* ipc stuff */

       struct sysv_sem sysvsem;

/* CPU-specific state of this task */

       struct thread_struct thread;

/* filesystem information */

       struct fs_struct *fs;

/* open file information */

       struct files_struct *files;

/* namespace */

       struct namespace *namespace;

/* signal handlers */

       struct signal_struct *signal;

       struct sighand_struct *sighand;

       sigset_t blocked, real_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;

      

       void *security;

       struct audit_context *audit_context;

/* Thread group tracking */

     u32 parent_exec_id;

     u32 self_exec_id;

/* Protection of (de-)allocation: mm, files, fs, tty, keyrings */

       spinlock_t alloc_lock;

/* Protection of proc_dentry: nesting proc_lock, dcache_lock, write_lock_irq(&tasklist_lock); */

       spinlock_t proc_lock;

/* context-switch lock */

       spinlock_t switch_lock;

/* journalling filesystem info */

       void *journal_info;

/* VM state */

       struct reclaim_state *reclaim_state;

       struct dentry *proc_dentry;

       struct backing_dev_info *backing_dev_info;

       struct io_context *io_context;

       unsigned long ptrace_message;

       siginfo_t *last_siginfo; /* For ptrace use.  */

       wait_queue_t *io_wait;

/* i/o counters(bytes read/written, #syscalls */

       u64 rchar, wchar, syscr, syscw;

};

3.2.2.1:进程描述符处理

创建进程的时候,系统将进程描述符放在内核堆中。

此外,每个进程都有两个数据结构,分别是和进程描述符相关的线程描述符(thread_info)和进程的内核态堆栈。

union thread_union {

       struct thread_info thread_info; // thread_info的第一个成员就是task结构体指针

       unsigned long stack[THREAD_SIZE/sizeof(long)]; //进程的内核态堆栈

};

       这个结构体通常大小为8K。

#define alloc_thread_info(tsk) kmalloc(THREAD_SIZE, GFP_KERNEL) //创建thread_info结构体

3.2.2.2:标识当前进程

       由于内核堆栈和线程描述符绑定在一起,当进入内核态的时候,很容易从当前的内核栈指针(此时的栈指针esp)获得线程描述符的地址。只需要屏蔽栈指针的低13位即可。

       在获得线程描述符的地址之后,线程描述符偏移为0的位置就是进程描述符的地址。

static inline struct thread_info *current_thread_info(void)

{

       struct thread_info *ti;

       __asm__("andl %%esp,%0; " //将0xffffe000与栈指针做与运算,得到当前thread_info的地址

:"=r" (ti)  //输出部分

: "0" (~(THREAD_SIZE - 1)));  //输入部分,这个值就是0xffffe000。

       return ti;

}

       thread_info的第一个成员就是task结构体的地址。因此可以通过当前栈指针的值方便的得到task结构体地址。

3.2.2.3:双向链表

       进程链表把所有进程描述符链接起来。每个task结构体中都有一个双向链表元素,分别指向前一个和后一个task。

       linux中的链表为了通用性与常用的链表结构有区别,Linux中链表结构如下:

struct list_head {

       struct list_head *next, *prev;

};

       可见他没有普通链表的数据域。Linux中的链表是作为结构体中的一个成员添加进去的。结构体中的其他成员起到了普通链表的数据域的作用。

       因此,对于Linux中,非常重要的一件事就是如果根据链表查到这个结构体的首地址,这里需要使用一个宏list_entry

#define list_entry(ptr, type, member) \

       container_of(ptr, type, member)

#define container_of(ptr, type, member) ({                     \

        const typeof( ((type *)0)->member ) *__mptr = (ptr);      \

        (type *)( (char *)__mptr - offsetof(type,member) );})

#define offsetof(type, member) ((size_t)( (char *)&(((type *)0)->member) - (char *)0 ))

       它的作用是,根据结构体的类型type,type中双向链表成员的指针ptr,type中双向链表成员的名字member,得到这个结构体的首地址。比如,对于下面的一个结构体:

struct A{

int a;

struct list_head list_item;

};

       这时候有A类型的结构体实体a,a. list_item的指针是list_item_ptr,那么使用

container_of(list_item_ptr,struct A, list_item)

就可以得到a的首地址。

       要注意的是,list_head是双向循环链表,即最后一个元素和第一个元素也是联系在一起的。

       此外,还有一种双向链表叫做散列链表。他不是循环链表。使用结构体hlist_head表示。

3.2.2.4:进程链表

       task结构体中的

struct list_head tasks

字段用于构成进程链表。他将所有的进程描述符链接起来。

       进程链表的头是init_task描述符,是第一个进程。他的前一个进程是系统中最后一个插入双向链表的进程。

3.2.2.4:TASK_RUNNING状态的进程链表

       task结构体中的

struct list_head run_list

字段用于构成可运行进程链表。

       Linux为了能够快速找到该运行的进程(优先级),使用数据结构

struct prio_array {

       unsigned int nr_active; //链表中进程描述符的数量

       unsigned long bitmap[BITMAP_SIZE];

       struct list_head queue[140];

};

来存储140个进程链表,分别对应的不同的优先级(0~139)。struct list_head queue[140]中链接的就是task结构体中的run_list成员。

3.2.3:进程的关系

       进程间还有其他关系,比如父子进程,兄弟进程。他们之间也通过task结构体中的链表成员进行连接。不在此叙述。

3.2.3.1:pidhash表和链表

       有时候需要根据进程的pid得到进程的描述符。比如在使用kill系统调用的时候。可以顺序扫描进程链表并且检查进程描述符中的pid的值,但是这样非常低效。

       为了加速查找,引入了四个散列表。每个散列表的散列依据不同,可以根据进程描述符中的pid,或者进程的tgid(线程组领头进程的pid)等进行散列。因此,一个进程会在四个散列表中都存在。四个散列表的首地址存在全局变量数组pid_hash中。

static struct hlist_head *pid_hash[PIDTYPE_MAX];

enum pid_type

{

       PIDTYPE_PID,

       PIDTYPE_TGID,

       PIDTYPE_PGID,

       PIDTYPE_SID,

       PIDTYPE_MAX

};

       每个散列表有n个表项,在对不同的PID进行散列的时候,可能发生哈希冲突。即,不同的PID对应了散列表中相同的表项。因此,在每个哈希散列表的表项后在增加一个双向链表。如下图所示:

 

每个PID散列表链接的是task结构体中的:

struct pid pids[PIDTYPE_MAX]

结构。

       通过函数attach_pid可以更加清晰的了解PID散列表的组织形式。

int fastcall attach_pid(task_t *task, enum pid_type type, int nr)

{

       struct pid *pid, *task_pid;

       task_pid = &task->pids[type];

       pid = find_pid(type, nr);

       if (pid == NULL) {

              hlist_add_head(&task_pid->pid_chain,

                            &pid_hash[type][pid_hashfn(nr)]);

              INIT_LIST_HEAD(&task_pid->pid_list);

       } else {

              INIT_HLIST_NODE(&task_pid->pid_chain);

              list_add_tail(&task_pid->pid_list, &pid->pid_list);

       }

       task_pid->nr = nr;

       return 0;

}

3.2.4:如何组织进程

3.2.4.1:等待队列

       等待不同的事件的进程会组成不同的等待队列。

等待队列包括两部分,分别是等待队列头和等待队列项。

       每个等待队列都有一个等待队列头。等待队列头:

struct __wait_queue_head {

       spinlock_t lock; //自旋锁

       struct list_head task_list; //等待进程链表的头

};

       lock是为了防止等待队列被进程同时访问而引入的,实现了进程间的同步。

       task_list是一个双向链表,用于指向此等待队列中的第一个等待队列项。

       等待队列项:

struct __wait_queue {

       unsigned int flags;

#define WQ_FLAG_EXCLUSIVE   0x01

       struct task_struct * task;

       wait_queue_func_t func;

       struct list_head task_list;

};

       每一个等待队列项都代表了一个睡眠进程。进程描述符放在task中,task_list连接了等待相同事件的进程构成的等待队列项。

       flags的作用如下:有的等待列表,是在等待到某一事件后,释放全部的等待进程(flags=0);而有的等待列表,是释放一个等待进程(flags=1)。

3.2.4.2:等待队列的操作

       如果要将一个进程插入等待队列中,有如下几个步骤。

       1:首先初始化一个等待队列头。如果已经有了需要的等待队列头,直接访问。

#define DECLARE_WAIT_QUEUE_HEAD(name)

       2:动态分配一个等待队列项,然后对其进行初始化

static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)

一般是对当前进程创建一个等待队列项。

3:将这个等待队列项加入等待队列中,其实就是和一个等待队列头联系起来。

void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

这个函数将这个等待队列项加入等待队列的第一个位置。

       以上是Linux中等待队列的实现原理,实际上Linux提供了相当多的接口,用于将当前进程根据不同的等待条件,加入不同的等待队列中。此外,也包含将等待队列中的进程释放的接口。

3.2.4.3:linux中的进程链表

       综合上述可知,linux中,为进程组织了五种不同的链表,分别是

       1:进程之间直接互联

       2:TASK_RUNNING状态的进程链表

       3:父子兄弟进程链表

       4:pidhash链表

       5:等待队列链表

      

       1:进程之间的直接互联:

       是通过task结构体中的成员struct list_head tasks实现的。第一个进程时init_task进程,以后每建立一个进程,都将该进程的task结构体添加到这个链表中。需要注意的是,如果添加一个轻量级进程,这个进程不会加入这个链表。

       2:TASK_RUNNING状态的进程链表:

       Linux中有一个结构体prio_array_t,管理了140个链表,表示的是不同优先级的ready队列。这些队列中进程的串联是通过进程结构体中的struct list_head run_list实现的。另外,task结构体中的prio_array_t *array指向的就是管理140个链表的结构体。

       3:父子兄弟进程链表:

       linux中是通过task结构体中的children和sibling来实现这种关系的,它们都是list_head类型的。children的next指向的是该进程最新的子进程,prev指向的是该该进程最老的子进程,sibling的next指向的是它父进程中比它更老的子进程,prev指向的是它父进程中比它更新的子进程。最新子进程的slibling.prev指向的是父进程,最老子进程的slibling.next也是指向父进程。

4:pidhash链表

       每个进程都可能同时处于四个不同类型的散列表中,在进程描述符中有一个成员数组为struct pid pids[PIDTYPE_MAX],不同的数组元素索引了不同类型的pid链表。每个pid链表元素有两个链表成员,一个是将相同pid链起来的pid_list,另一个是链接发生哈希冲突的pid_chain。

       5:等待队列

       如上所述。

3.2.5:进程资源限制

       每个进程都有资源限制,限制他可以使用的最大系统资源。

       这个限制存储在task->signal->rlim中。

3.3:进程切换

3.3.1:硬件上下文

       进程恢复执行前必须装入寄存器的一组数据叫做硬件上下文。在linux中,硬件上下文的一部分放在TSS中,剩余部分放在内核态堆栈中。

       进程切换只会发生在内核态中,在执行进程切换之前,用户态进程使用的所有寄存器内容都已经保存在了内核态的堆栈上。

3.3.2:任务状态段

       Linux强制每个CPU都有一个TSS段(任务状态段)用于存放硬件上下文。当x86的一个CPU从用户态进入内核态的时候,都会在TSS中获取内核态的堆栈地址。TSS的格式在结构体中

struct tss_struct

       TSS反映了当前运行在CPU上的进程的信息。     

       TSS段的段描述符在全局描述符表GDT中。TSS段的段选择符的地址放在CPU的tr寄存器中。所以可以直接通过tr寄存器找到TSS段。

3.3.2.1:thread字段

       当一个进程被切出的时候,需要在一个地方保存进程的硬件上下文。因此,每个进程描述符都包括一个结构体成员:

struct thread_struct {

/* cached TLS descriptors. */

       struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES];

       unsigned long      esp0;

       unsigned long      sysenter_cs;

       unsigned long      eip;

       unsigned long      esp;

       unsigned long      fs;

       unsigned long      gs;

/* Hardware debugging registers */

       unsigned long      debugreg[8];  /* %%db0-7 debug registers */

/* fault info */

       unsigned long      cr2, trap_no, error_code;

/* floating point info */

       union i387_union i387;

/* virtual 86 mode info */

       struct vm86_struct __user * vm86_info;

       unsigned long             screen_bitmap;

       unsigned long             v86flags, v86mask, saved_esp0;

       unsigned int         saved_fs, saved_gs;

/* IO permissions */

       unsigned long      *io_bitmap_ptr;

/* max allowed port in the bitmap, in bytes: */

       unsigned long      io_bitmap_max;

};

进程的大部分硬件上下文都被保存在这个结构体中,但是对于一些非易失性存储器,他们的值保存在内核堆栈中。

3.3.3:执行进程切换

       从本质上来讲,进程切换包含两个部分:

       1:切换全局页目录,安装一个新的地址空间。

       2:切换内核态堆栈和硬件上下文。

       这里可以有一个简单的认识,进程的切换总共需要三个函数(虽然他们之间是相互调用的关系)

       1:context_switch():切换全局页表项。

       2:switch_to宏:切换内核堆栈和进程。

       3:__switch_to函数:切换硬件上下文

3.3.3.1:switch_to宏

       switch_to内联汇编代码如下:

#define switch_to(prev, next, last)     \

do {         \

/*        \

  * Context-switching clobbers all registers, so we clobber \

  * them explicitly, via unused output variables.  \

  * (EAX and EBP is not listed because EBP is saved/restored \

  * explicitly for wchan access and EAX is the return value of \

  * __switch_to())      \

  */        \

unsigned long ebx, ecx, edx, esi, edi;    \

         \

asm volatile("pushfl\n\t"  /* save    flags */ \

       "pushl %%ebp\n\t"  /* save    EBP   */ \

       "movl %%esp,%[prev_sp]\n\t" /* save    ESP   */ \

       "movl %[next_sp],%%esp\n\t" /* restore ESP   */ \

       "movl $1f,%[prev_ip]\n\t" /* save    EIP   */ \

       "pushl %[next_ip]\n\t" /* restore EIP   */ \

       "jmp __switch_to\n" /* regparm call  */ \

       "1:\t"      \

       "popl %%ebp\n\t"  /* restore EBP   */ \

       "popfl\n"   /* restore flags */ \

         \

       /* output parameters */                       \

       : [prev_sp] "=m" (prev->thread.sp),  \

       /*m表示把变量放入内存,即把[prev_sp]存储的变量放入内存,最后再写入prev->thread.sp*/\

         [prev_ip] "=m" (prev->thread.ip),  \

         "=a" (last),                                           \

         /*=表示输出,a表示把变量last放入ax,eax = last*/         \

         \

         /* clobbered output registers: */  \

         "=b" (ebx), "=c" (ecx), "=d" (edx),  \

         /*b 变量放入ebx,c表示放入ecx,d放入edx,S放入si,D放入edi*/\

         "=S" (esi), "=D" (edi)    \

                \

         /* input parameters: */    \

       : [next_sp]  "m" (next->thread.sp),  \

       /*next->thread.sp 放入内存中的[next_sp]*/\

         [next_ip]  "m" (next->thread.ip),  \

                \

         /* regparm parameters for __switch_to(): */ \

         [prev]     "a" (prev),    \

         /*eax = prev  edx = next*/\

         [next]     "d" (next)    \

         \

       : /* reloaded segment registers */   \

   "memory");     \

} while (0)

       首先需要说明内联汇编的格式:

asm volatile("汇编语句"

     : 输出部分 :表示last在eax寄存器输出

     : 输入部分 :next放在esi中,prev放在edx中,剩下的都是立即数

     : 会被修改的部分); :执行上述汇编指令会对这些地方的内容造成改动

       将这段指令中关联的寄存器列出如下

rflags:32位系统中为eflags。是状态与控制寄存器,用于提供程序的状态及进行相应的控制

ebp:帧指针,指向栈的开始

esp:栈指针,指向栈顶

需要对这两个栈指针作说明:pop/push这样的操作,都是对esp所指向的堆栈进行的,这些操作同时也会改变esp本身。而局部变量的索引都是对ebp所指向的堆栈进行的,在编译的时候,每个局部变量都会表示成n(%ebp)的格式,并且,如果不同进程共享同一代码段,他们在这个代码段中的同一局部变量,汇编出来的n的值应该是相同的。。

       这段代码实际上的操作步骤如下所示:

[prev]     "a" (prev),  

[next]     "d" (next)

将prev指针放入eax中,next指针放入edx中。要注意这里的prev和next都是进程A的局部变量。

pushfl

pushl %%ebp

将rflags和ebp寄存器的值保存到栈中。这时候使用的栈还是进程A的栈。

movl %%esp,%[prev_sp]

将当前的栈指针存在进程A的PCB中。

movl %[next_sp],%%esp

将进程B的PCB中的栈指针存在esp寄存器中。注意此时栈指针完成了切换,帧指针还没有完成。由于Linux中通过栈指针判断当前的进程,因此这里实际上切换到了B进程。有必要对此进行更深的说明,由于这段代码是所有进程通用的,因此,判断当前在哪一个进程不是看哪个进程执行的这段代码,而是看esp栈指针。

movl $1f,%[prev_ip]

将1f地址存到进程A的PCB中的IP指针处。当A重新开始运行的时候,从此处执行。

pushl %[next_ip]

将进程B的PCB中的IP指针压入栈中,此时是压入进程B的内核栈

jmp __switch_to

进入函数__switch_to,当这个函数返回的时候,会将刚刚压入B进程的内核栈的,即B进程PCB中的IP指针作为返回地址。

1:

从__switch_to返回到这里的时候,实际上已经是进程B的代码段的1了。

popl %%ebp//将B进程的PCB中的ebp放入ebp寄存器中

popfl

由于之前已经将内核栈从A切换到了B,现在的栈指针指向的是B的内核栈,因此,这里弹出的时候实际上是弹出的B的内核栈中存储的信息(也是上一次B被切走的时候存储在B的内核栈中的)

当ebp发生切换的时候,局部变量的值已经全部变成了B的内核栈中存储的局部变量的值了。

"=a" (last)

switch_to函数最终结束的时候,会返回一个值给调用它的函数,这个值就是寄存器eax中的值,也是进程A的PCB的首地址。

3.3.3.2:__switch_to函数

用于切换硬件上下文

struct task_struct fastcall * __switch_to(struct task_struct *prev_p, struct task_struct *next_p)

{

       struct thread_struct *prev = &prev_p->thread,

                             *next = &next_p->thread;

       int cpu = smp_processor_id();

       struct tss_struct *tss = &per_cpu(init_tss, cpu);//拿到当前CPU的TSS结构体

       __unlazy_fpu(prev_p);

       /*

        * Reload esp0, LDT and the page table pointer:

        */

       load_esp0(tss, next);//把B进程的PCB中的esp0指针存入tss中

       /*

        * Load the per-thread Thread-Local Storage descriptor.

        */

       load_TLS(next, cpu);

       /*

        * Save away %fs and %gs. No need to save %es and %ds, as

        * those are always kernel segments while inside the kernel.

        */

       asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));

       asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));//保存fs,gs段寄存器

       return prev_p;

}

       需要注意的是,return翻译成汇编就是ret,它的作用就是将此时栈顶保存的地址放入IP寄存器中。而在调用__switch_to的时候,是将B进程PCB中的%[next_ip]保存在栈顶,因此此时返回到%[next_ip]处执行。

3.3.4:保存和加载FPU、MMU、XMM寄存器

      

3.4:创建进程

       现代Unix内核有以下的三种特性,用于实现进程的创建:

       1:写时复制机制:允许父进程子进程读相同的物理页。两者中只要有一个试图写物理页,内核就把这个页的内容拷贝到一个新的物理页,并且将这个新的物理页分配给正在写的进程。

       2:轻量级进程允许父子进程共享进程在内核的很多数据结构,包括页表,打开的文件和信号处理。

       3:vfork()系统调用创建的进程能够共享父进程的内存地址空间。并且阻塞父进程的运行,直到子进程退出或者执行一个新的进程。

3.4.1:clone(),fork(),vfork()系统调用

       clone是封装起来的一个例程,需要传入创建的进程要执行的函数fn和参数argv。但是实现clone的系统调用sys_clone并没有这两个参数,实际上clone将这两个参数存放在了子进程堆栈的某个位置。clone的实现在glibc库中。系统调用过程在后续介绍。

3.4.1.1:do_fork函数

       do_fork函数是这三个系统调用的实际实现。

long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr)

{

       struct task_struct *p;

       int trace = 0;

       long pid = alloc_pidmap();//为子进程分配新的pid

       if (pid < 0)

              return -EAGAIN;

       p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);//创建进程描述符

       if (!IS_ERR(p)) {

              struct completion vfork;

              if (clone_flags & CLONE_VFORK) {

                     p->vfork_done = &vfork;

                     init_completion(&vfork);

              }

              if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {

                     /*

                      * We'll start up with an immediate SIGSTOP.

                      */

                     sigaddset(&p->pending.signal, SIGSTOP);

                     set_tsk_thread_flag(p, TIF_SIGPENDING);

              }

              if (!(clone_flags & CLONE_STOPPED))

                     wake_up_new_task(p, clone_flags);

              else

                     p->state = TASK_STOPPED;

              if (clone_flags & CLONE_VFORK) {

                     wait_for_completion(&vfork);

                     if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))

                            ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);

              }

       } else {

              free_pidmap(pid);

              pid = PTR_ERR(p);

       }

       return pid;

}

       使用函数copy_process创建子进程的进程描述符

static task_t *copy_process(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr, int pid)

{

       int retval;

       struct task_struct *p = NULL;

       if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))

              return ERR_PTR(-EINVAL);

       if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))

              return ERR_PTR(-EINVAL);

       if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))

              return ERR_PTR(-EINVAL);

//检查clone_flags一致性,里面有一些互斥的位

       retval = security_task_create(clone_flags);//安全性检查,只有在开启安全编译选项后会有。不重要,不分析

       if (retval)

              goto fork_out;

       retval = -ENOMEM;

       p = dup_task_struct(current);//分配子进程的进程描述符和线程描述符

       if (!p)

              goto fork_out;

       retval = -EAGAIN;

       if (atomic_read(&p->user->processes) >=

                     p->signal->rlim[RLIMIT_NPROC].rlim_cur) {

              if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&

                            p->user != &root_user)

                     goto bad_fork_free;

       }//检查进程是否超过进程限制

       atomic_inc(&p->user->__count);

       atomic_inc(&p->user->processes);

       get_group_info(p->group_info);//递增这些参数,不知道具体含义

       if (nr_threads >= max_threads)

              goto bad_fork_cleanup_count;//进程数目不得超过限制的数目

       if (!try_module_get(p->thread_info->exec_domain->module))

              goto bad_fork_cleanup_count;

       if (p->binfmt && !try_module_get(p->binfmt->module))

              goto bad_fork_cleanup_put_domain;

       p->did_exec = 0;

       copy_flags(clone_flags, p);//设置进程的flags

       p->pid = pid;//在do_fork中获得的pid

       retval = -EFAULT;

       if (clone_flags & CLONE_PARENT_SETTID)

              if (put_user(p->pid, parent_tidptr))

                     goto bad_fork_cleanup;

       p->proc_dentry = NULL;

       INIT_LIST_HEAD(&p->children);//将子进程加入进程链表中

       INIT_LIST_HEAD(&p->sibling);

       p->vfork_done = NULL;

       spin_lock_init(&p->alloc_lock);

       spin_lock_init(&p->proc_lock);

       clear_tsk_thread_flag(p, TIF_SIGPENDING);

       init_sigpending(&p->pending);

    ……//设置挂起信号,定时器以及时间统计表等相关字段

       p->tgid = p->pid;

       if (clone_flags & CLONE_THREAD)

              p->tgid = current->tgid;// CLONE_THREAD:属于同一个线程组。将子进程的线程组号设置为父进程的线程组号

//下列函数用于从父进程的PCB中复制一些数据结构到子进程的PCB中。是否复制由clone_flags决定,不同的位决定了父子进程间有多少共享内容

       /* copy all the process information */

       if ((retval = copy_files(clone_flags, p)))

              goto bad_fork_cleanup_semundo;

       if ((retval = copy_fs(clone_flags, p)))

              goto bad_fork_cleanup_files;

       if ((retval = copy_sighand(clone_flags, p)))

              goto bad_fork_cleanup_fs;

       if ((retval = copy_signal(clone_flags, p)))

              goto bad_fork_cleanup_sighand;

       if ((retval = copy_mm(clone_flags, p)))

              goto bad_fork_cleanup_signal;

       if ((retval = copy_keys(clone_flags, p)))

              goto bad_fork_cleanup_mm;

       if ((retval = copy_namespace(clone_flags, p)))

              goto bad_fork_cleanup_keys;

       retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);

       if (retval)

              goto bad_fork_cleanup_namespace;

       p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;

       /*

        * Clear TID on mm_release()?

        */

       p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;

       /*

        * Syscall tracing should be turned off in the child regardless

        * of CLONE_PTRACE.

        */

       clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);

       /* Our parent execution domain becomes current domain

          These must match for thread signalling to apply */

         

       p->parent_exec_id = p->self_exec_id;

       /* ok, now we should be set up.. */

       p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);

       p->pdeath_signal = 0;

       p->exit_state = 0;

       /* Perform scheduler related setup */

       sched_fork(p);

       /*

        * Ok, make it visible to the rest of the system.

        * We dont wake it up yet.

        */

       p->group_leader = p;

       INIT_LIST_HEAD(&p->ptrace_children);

       INIT_LIST_HEAD(&p->ptrace_list);

       /* Need tasklist lock for parent etc handling! */

       write_lock_irq(&tasklist_lock);

       /*

        * The task hasn't been attached yet, so cpus_allowed mask cannot

        * have changed. The cpus_allowed mask of the parent may have

        * changed after it was copied first time, and it may then move to

        * another CPU - so we re-copy it here and set the child's CPU to

        * the parent's CPU. This avoids alot of nasty races.

        */

       p->cpus_allowed = current->cpus_allowed;

       set_task_cpu(p, smp_processor_id());

       /*

        * Check for pending SIGKILL! The new thread should not be allowed

        * to slip out of an OOM kill. (or normal SIGKILL.)

        */

       if (sigismember(&current->pending.signal, SIGKILL)) {

              write_unlock_irq(&tasklist_lock);

              retval = -EINTR;

              goto bad_fork_cleanup_namespace;

       }

       /* CLONE_PARENT re-uses the old parent */

       if (clone_flags & (CLONE_PARENT|CLONE_THREAD))

              p->real_parent = current->real_parent;

       else

              p->real_parent = current;

       p->parent = p->real_parent;

       if (clone_flags & CLONE_THREAD) {

              spin_lock(&current->sighand->siglock);

              /*

               * Important: if an exit-all has been started then

               * do not create this new thread - the whole thread

               * group is supposed to exit anyway.

               */

              if (current->signal->flags & SIGNAL_GROUP_EXIT) {

                     spin_unlock(&current->sighand->siglock);

                     write_unlock_irq(&tasklist_lock);

                     retval = -EAGAIN;

                     goto bad_fork_cleanup_namespace;

              }

              p->group_leader = current->group_leader;

              if (current->signal->group_stop_count > 0) {

                     /*

                      * There is an all-stop in progress for the group.

                      * We ourselves will stop as soon as we check signals.

                      * Make the new thread part of that group stop too.

                      */

                     current->signal->group_stop_count++;

                     set_tsk_thread_flag(p, TIF_SIGPENDING);

              }

              spin_unlock(&current->sighand->siglock);

       }

       SET_LINKS(p);

       attach_pid(p, PIDTYPE_PID, p->pid);

       attach_pid(p, PIDTYPE_TGID, p->tgid);

       if (thread_group_leader(p)) {

              attach_pid(p, PIDTYPE_PGID, process_group(p));

              attach_pid(p, PIDTYPE_SID, p->signal->session);

              if (p->pid)

                     __get_cpu_var(process_counts)++;

       }

       nr_threads++;

       total_forks++;

       write_unlock_irq(&tasklist_lock);

       retval = 0;

fork_out:

       if (retval)

              return ERR_PTR(retval);

       return p;

}

       使用到函数

static struct task_struct *dup_task_struct(struct task_struct *orig)

{

       struct task_struct *tsk;

       struct thread_info *ti;

       prepare_to_copy(orig);

       tsk = alloc_task_struct();//通过slab分配器分配进程描述符结构体。slab分配器将在后面描述

       if (!tsk)

              return NULL;

       ti = alloc_thread_info(tsk);//分配线程描述符

       if (!ti) {

              free_task_struct(tsk);

              return NULL;

       }

       *ti = *orig->thread_info;

       *tsk = *orig;

       tsk->thread_info = ti;

       ti->task = tsk;

       /* One for us, one for whoever does the "release_task()" (usually parent) */

       atomic_set(&tsk->usage,2);

       return tsk;

}

3.4.2:内核线程

       内核线程只运行在内核态,而普通进程可以运行在内核态或者用户态。此外,内核线程只会使用大于3G的线性地址空间。

3.4.2.1:创建一个内核线程

       kernel_thread创建一个内核线程

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)

{

       struct pt_regs regs;

       memset(&regs, 0, sizeof(regs));

       regs.ebx = (unsigned long) fn;

       regs.edx = (unsigned long) arg;

       regs.xds = __USER_DS;

       regs.xes = __USER_DS;

       regs.orig_eax = -1;

       regs.eip = (unsigned long) kernel_thread_helper;

       regs.xcs = __KERNEL_CS;

       regs.eflags = X86_EFLAGS_IF | X86_EFLAGS_SF | X86_EFLAGS_PF | 0x2;

       /* Ok, create the new process.. */

       return do_fork(flags | CLONE_VM | CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);// CLONE_VM避免复制调用进程的页表。CLONE_UNTRACED保证不会有进程跟踪新建的内核线程。

}

3.4.2.2:进程0

       进程0的初始化使用如下语句

struct task_struct init_task = INIT_TASK(init_task);//全局变量init_task

union thread_union init_thread_union

       __attribute__((__section__(".data.init_task"))) =

              { INIT_THREAD_INFO(init_task) };

//全局变量init_thread_union,初始化thread_union

struct mm_struct init_mm = INIT_MM(init_mm);//全局变量init_mm

       start_kernel函数初始化内核需要的所有数据结构,激活中断,创建PID为1的进程(init进程)

static int init(void * unused)

       进程0在函数rest_init中创建进程1,并且在创建完成后进行主动调度。

static void noinline rest_init(void)

       __releases(kernel_lock)

{

       kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);//创建进程1

       numa_default_policy();

       unlock_kernel();

       preempt_enable_no_resched();

       cpu_idle(); //进行调度

}

       在多处理器系统中,每个CPU都有一个进程0.打开机器电源后,BIOS会启动一个CPU,然后禁用其他CPU。运行在CPU0上的swapper进程初始化内核数据结构后,激活其他CPU,然后创建其他CPU的swpper进程。

3.5:撤销进程

3.5.1:进程终止

       有两个终止用户态应用的系统调用:

       1:exit_group():这个系统调用终止整个线程组,即基于多线程的应用。他在内核中的实现是通过do_group_exit函数实现的。

       2:exit():这个系统调用终止一个线程,而不影响线程组中其他的进程。他在内核中的实现是通过do_exit()实现的。

3.5.1.1:do_group_exit()函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值