博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

[OS]Process

Posted on 2010-04-02 18:05  xuczhang  阅读(651)  评论(0编辑  收藏  举报

 

进程的静态特征

1. task_struct

image

2. task state进程状态

3. PID 与tgid

tgid是线程组的id,pid是当前进程id,而需要注意的是getpid()得到的是tgid。线程组就是轻量级进程的集合。tgid就是组中第一个轻量级进程的pid

4. current宏与内核栈

Question:进程内核栈??? task_union

image

Answer:如上图可以看到内核栈和task_struct是连在一起的,一共是2个PAGE_SIZE=8KB,也就是下图所示的THREAD_SIZE,alloc_task_struct就是请求2个页的空间,其中1KB左右给task_struct,另外的7KB给内核栈。也就是说这里的内核栈是每个进程一个,当内核在当前进程运行的时候就会使用这个内核栈,内核栈的大小不同于用户栈是不可以更改的,也就是说内核不能在栈上分配大的局部变量和深的函数调用。

BTW,__get_free_pages中的"1”表示2的一次方,就是2个page。

image

下面我们来说下current宏

image

current宏就是当前执行的进程的task_struct,它是通过当前栈的指针(内核栈)计算出来的:AND上0xfffffe00

 

5. 进程链表

由task_struct中的next_task和prev_task串起。

由init进程的task_struct开头,通过for_each_task宏来遍历。

 

6.pidhash

通过这个hash表快速的用pid找到相应的task_struct。list通过task_struct中的pidhash_next和pidhash_prev链接。

image

7.进程间的亲属关系

task_struct中的这几个指针描述了进程间的亲属关系

image

image

 

8. run_list

run_list是对应状态为TASK_RUNNING的进程链表,在task_struct中是类型为list_head的run_list变量。

 

9. Wait Queue

等待队列是用来管理状态为TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE的进程的。

wait_queue_head_t: 等待队列的头。lock+list_head

image

wait_queue_t:等待队列元素。flags+task_struct+task_list.

flag=0表示非互斥的进程,flag=1表示互斥的进程。有时互斥进程只需要唤醒一个就可以了,因为互斥的资源只有一个。

image

内核提供了如下等待队列的操作:

1). init wait queue

2). add/remove from wait queue

3). 每个等待队列中的进程可以调用sleep/wait类型的操作来使进程睡眠或者唤醒。

Question:有哪些类型的wait queue?比如为每个锁每个资源建立一个等待队列?

Answer: 一个wait queue的例子就是semaphore,在semaphore中有一个wait变量是一个wait_queue。

 image

10. 进程资源限制

struct rlimit来记录当前和最大的资源描述。

image

在task_struct中有一个rlimit的数组对应所有的资源限制。

image

具体的数组中的项的含义由以下宏定义:

image 

 

进程切换

1. TSS

TSS的结构如下图所示,tss在i386的设计上是每个进程一个,而linux中是每个CPU对应一个,它会存放在init_tss数组中。

image

Question: linux use TSS to store the process’s context when schedule?? NO!!! but How?

Answer: i386提供了一种每个进程一个TSS的机制来切换进程,进程的切换通过中断或者JMP和CALL指令由硬件自动完成,不过linux没有采用这种机制,不过由于i386CPU要求软件设置TR及TSS,内核只好“走过场”地设置好TR及TSS以满足CPU的要求。不使用硬件提供的进程切换机制,而选择自己去完成调度的原因是:

(1)看似由硬件完成进程切换比较高效,其实不然,由硬件完成的这一系列切换需要长达300多个CPU时钟周期,而这个过程并不是每个步骤都是必要的。

(2)另外进程的切换常常要与操作系统其他的部分发生关系,操作系统可以对那些部分进行优化。

所以Linux选择自己来实现进程切换,通过switch_to宏来完成。

2. thread_struct

当每次进程切换的时候,进程的hardware context必须保存。不过不能像Intel最初设计的那样保存在TSS(Intel原来设计每个进程一个TSS,而linux是一个CPU一个TSS),不过我们无法猜测被替换进程什么时候恢复执行,哪一个CPU又将执行它。

所以需要thread_struct,它保存在task_struct的thread变量中,这个结构包含了大部分的CPU寄存器:

image

3. 进程切换步骤

(1)切换页全局目录

(2)切换内核态堆栈和硬件文境

由switch_to宏执行

image

__switch_to

进程创建

1. fork(),clone(),vfork()

子进程拥有自己的task_struct和系统空间堆栈,但与父进程共享其他所有资源。fork和clone的区别在于fork复制全部,不过clone可以将资源有选择的复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享。事实上fork就是clone()的一种特例,fork()是通过调用clone()来实现的。vfork()能共享父进程的内存地址空间,为了防止父进程重写子进程需要的数据,阻塞父进程的执行,直到子进程退出或执行一个新的程序。vfork()主要是出于效率的考虑而设计并提供的。相关代码如下:

这是在do_fork()中的代码,如果设定了CLONE_VFORK的flag就将父进程阻塞在vfork上。vfork是一个类型为completion的变量(在do_fork()中定义)

image

image

2. do_fork()

clone(),fork(),vfork()都调用do_fork()

image

do_fork()比较复杂,在ULK中它包括22个步骤,可以概括为:

1. 分配自己的task_struct(包括内核栈)

2. 从父进程复制task_struct的变量

3. 检查一些资源限制

4. 创建子进程的一些资源,通过copy_files(),copy_fs(),copy_sighand()及copy_mm()来实现。

5. 通过调用copy_thread()来初始化子进程的内核栈

6. 把新进程插入到进程链表

7.调用wake_up_process()把子进程的state置为TASK_RUNNING,并把子进程插入到run_list中。

8.如果指定了CLONE_VFORK,就阻塞父进程直到子进程退出或执行一个新的程序。

在完成了do_fork后,子进程已经被加入到run_list中,并等待调度程序来调度到它。在进行进程切换时,会把子进程的thread中的值装入CPU寄存器。特别是把tread.esp装入esp寄存器(代表内核栈的地址),把函数ret_from_fork()的地址装入eip寄存器。然后调用ret_from_sys_call()来用栈中的值再装载所有的寄存器,并强迫CPU返回用户态。

Question: 子进程会共享父进程的全局变量,堆,代码段等,而不共享stack,这是如何实现的?

Answer: 当调用do_fork时加上CLONE_VM的flag,就会调用copy_mm()直接复制mm的指针。

image

那stack是如何实现不共享的?这里要分两种情况,一种是用户调用clone的系统调用时提供了栈的指针,do_fork()时把这个指针复制给

内核线程

每个内核进程执行一个单独的内核c函数。内核线程只运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间。创建内核进程是通过调用kernel_thread()来创建。

image

它是通过调用不同平台下的arch_kernel_thread()函数来实现的。

Question: 内核线程有自己的task_struct吗?

TODO:有

在ULK中提到kernel_thread()在某种程度上等价于下面代码

删除进程

Question: what happens when parent process terminate before child process terminate?

Answer: 在Linux2.6中对进程的删除有两种方式:

1. exit_group()。会遍历相同tgid的所有进程,给他们发送SIGKILL给这些进程。这个系统调用会在c库中的exit()中被调用。

Question: 如果exit_group()在exit中被调用,那每个子进程结束的时候调用exit()会kill父进程吗?

Answer: 不会。下面是zap_other_threads的片段(exit_group()会调用到zap_other_threads()),它会判断是否是group_leader。

image

2. _exit()

 

父子进程exit的关系

1. 子进程在父进程之前结束时,子进程成为Zombie进程,其task_struct不会被销毁直到父进程调用wait(),或者父进程终止。

2. 父进程在子进程之前结束,子进程会把自己的父进程设置为init进程,init进程会为每一个这样的进程调用wait()来释放其task_struct。