1.进程、轻量级进程和线程

进程(CPU时间、内存等)当一个进程创建时,它获得一个父进程地址空间的副本。共享正文段(代码段)Linux2.进程描述符(process descriptor)

为了管理进程,内核必须对每个进程所做的事已经处于的状态进行清楚的描述。进程描述符是task_struct类型结构,它的字段包含了与一个进程相关的所有信息。它不仅包含了很多进程属性的字段,还包含了指向其他数据结构的指针。

(1)进程状态

Task_struct的state字段描述了进程当前所处的状态,Linux中这些标志是互斥的。

①运行状态(TASK_RUNNING)

进程要么正在执行,要么准备执行

②可中断的等待状态(TASK_INTERRUPTIBLE)

进程被挂起(),知道某个条件为真,产生一个硬件中断,释放进程正等待的系统资源,或传递一个信号都是可以唤醒进程的条件(把进程状态改为TASK_RUNNING)

③不可中断的等待状态(TASK_UNINTERRUPTIBLE)

与TASK_INTERRUPTIBLE④暂停状态 (TASK_STOPPED)

进程的执行被暂停,当进程接收到SIGSTOP,SIGTSTP,SIGTTIN或SIGTTOU信号后,进入暂停状态

⑤跟踪状态(TASK_TRACED)

进程的执行被debugger(debuggerptrace())TASK_TRACED⑥僵死状态(EXIT_ZOMBIE)            

进程的执行被终止,但是,父进程还没有发布wait4()或waitped()系统调用来返回有关死亡进程的信息。

在发布wait()⑦僵死撤销状态( EXIT_DEAD) 

最终状态:由于父进程发出wait4()waitped(),内核常用一个简单的赋值语句设置state字段

p->state = TASK_RUNNING;

内核也使用set_task_stateset_current_state编译程序或CPU控制单元不把赋值操作与其他指令混合。

 

(2)标识一个进程

一般来讲,能被独立调度的每个执行上下文都必须拥有自己的进程描述符。

由于进程和进程描述符有严格的一一对应关系,那么使用32位的进程描述符的地址来标识进程成为一种方便的方式,事实上Linux内核中,进程描述符指针指向这些地址,对进程的大部分引用是通过进程描述符指针进行的。

另一方面,类Unix(precess ID,PID)PIDpid.PIDPIDPID1。不过当内核使用的PID达到上限值时就必须开始循环使用已闲置的小PID号。

缺省情况下,最大PID32767(PID_MAX_DEFAULT-1),/proc/sys/kernel/pid_maxPID64位系统中PID上限可以扩大到4194303

内核管理一个pidmap-arrayPIDPID32位体系结构中pidmap-array位图存放在一个单独的页中。Linux把不同的PID另一方面,POSIXPID.Linux线程组。一个线程组中的所有线程使用和该线程组的领头线程(thread group leader)相同的PID,也就是该组中第一个轻量级进程的PID,它被存在进程描述符的tgidgetpid()tgidpidPIDtgidpid值相同。

(3)进程描述符处理

对每个进程来说,Linux(thread_info),这块通常是8K

Esp寄存器是CPU栈指针,用来存放栈顶单元的地址。一旦数据写入堆栈,esp值就递减,因为thread_info52个字节,所以啮合栈能扩展到8140字节。

内核用这个联合结构方便的表示一个进程的线程描述符和内核栈。

union thread_union {

       struct thread_info thread_info;

       unsigned long stack[THREAD_SIZE/sizeof(long)];

};

内核用alloc_thread_infofree_thread_infothread_info 

(4)标志当前进程

Thread_info结构与内核态堆栈之间的紧密结合提供的好处是:内核很容易从esp寄存器的值获得当前在CPUthread_infothread_union8K,则屏蔽掉esp的低13thread_infocurrent_thread_info()进程最常用的是进程描述符地址,而不是thread_info结构的地址,为了获得当前运行进程的描述符指针,内核调用current宏,该宏本质上等价于current_thread_info()->task;current->pid返回在CPU上正执行的进程的PID用栈存放进程描述符的另一个优点是在多处理器系统上,对于每个硬件处理器,仅通过检查栈就可以获得当前正确的进程。有必要定义一个current数组,每个元素对应一个可用CPU.

(5)双向链表

Linux定义了list_head数据结构,LIST_HEAD(list_name)宏创建一个名字为list_name的双向链表,还初始化list_head数据机构的prev和nextlist_headlist_headlist_head双向链表详细分析见​​http://blog.chinaunix.net/uid-24708340-id-3235017.html​

Linux2.6支持另一种双向链表,不是循环链表,主要用于散列表,对散列表而言重要的是空间而不是在固定时间内好到表中的最后一个元素。表头存放在hlist_head结构中。第一个元素的pprev字段和最后一个元素的nextNULL.

①进程链表

进程链表把所有进程的描述符链接起来,每个task_struct都包含一个list_head类型的tasks字段。

表头是init_task0进程的进程描述符。Init_task的tasks.prev字段指向链表中最后插入的进程描述符的tasks字段。SET_LINKSREMOVE_LINKS#define for_each_process(p) \

       for (p = &init_task ; (p = next_task(p)) != &init_task ; )

这个宏用来遍历进程链表。每一次循环p存放的是被扫描进程描述符的地址,与list_entry宏返回值一样。

②TASK_RUNNING状态的进程链表

Linux2.6实现的运行队列与早期不同,其目的是让调度程序能在固定的时间内选出“最佳”可运行进程,与队列中可运行的进程数无关,是一个典型的用数据结构更复杂来改善性能的例子。

提高调度程序运行速度诀窍是,建立多个可运行进程链表,每种进程优先权对应一个不同的链表,每个task_struct描述符包含一个list_head类型的字段run_list.如果进程的优先权等于K(0~139),run_list字段吧该进程连入优先权为K的可运行进程链表中。在多处理器系统中,每个CPU都有它自己的运行队列,即自己的进程链表集。这样调度程序运行效率高了,但运行队列的链表被拆分成了140个不同的队列。所有这些链表都有一个单独的prio_array_t数据结构来实现。

struct prio_array {

       DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */

       struct list_head queue[MAX_RT_PRIO];

};

enqueue_task(struct rq *rq, struct task_struct *p, int wakeup, u64 now)函数把进程描述符插入某个运行队列的链表, dequeue_task(struct rq *rq, struct task_struct *p, int sleep, u64 now)从运行的队列中删除一个进程描述符。

(6)进程间的关系

②PIDP1P2P1kill()P2PID,内核从这个PID到处其对应的进程描述符,然后从P2的进程描述符中取出记录挂起信号的数据结构指针。

顺序扫描进程描述符表并检查进程描述符的id字段是可行但相对低效的,为了加速查找,引入了4个散列表

内核初始化期间动态地为4pid_hash散列表的详细分析见:​​http://blog.chinaunix.net/uid-24708340-id-3307281.html​

Linux利用链表来处理冲突的PID:每个表项石油冲突的进程描述符组成的双向链表,如下图,进程号为2892和29384200293​

具有链表的散列法比从PID3276832768PID散列表的数据机构还为每组线程组(pid相同)并且内核为每个PIDpidpid

此处)折叠或打开


  1. struct pid
  2. {
  3. int nr; //pid的数值
  4. ;//链接散列链表的下一个和前一个元素

  5. ;//每个pid的进程链表头
  6. };


下面是处理PID

此处)折叠或打开


  1. do_each_task_pid(nr,type,task)

  2. while_each_task_pid(nr,type,task)

循环作用在PIDnr的类型为type的链表中,task

此处)折叠或打开


  1. find_task_by_pid_type(type,nr)

  2. find_task_by_pid(nr)==find_task_by_pid_type(PIDTYPE_PID,nr)

  3. attach_pid(task,type,nr)

  4. detach_pid(task,type)

  5. next_thread(task)


(7)如何组织进程

①TASK_RUNNINGLinux没有为处于TASK_STOPPED,EXIT_ZOMBIEEXIT_EADPID,处于TASK_INTERRUPTIBLE or TASK_UNINTERRUPTIBLE状态的进程细分为很多类,每一类都响应一个特殊事件,这个过程中进程没有迅速取回足够的信息。所以有必要介绍另外的进程链表。

②等待队列在内核中主要用在中断处理、进程同步及定时,表示进程等待某些事件的发生。等待队列表示一组睡眠的进程,当某一条件为真时,由内核唤醒它们。

等待队列由双向链表实现,其元素包含指向进程描述符的指针,每个等待队列都有一个等待队列头。

此处)折叠或打开


  1. struct __wait_queue_head {

  2. ;

  3. ;

  4. };

typedef struct __wait_queue_head wait_queue_head_t;同步是通过等待队列头中的lock自旋锁达到的,task_list等待队列链表中的元素是

此处)折叠或打开


  1. struct __wait_queue {

  2. int flags;

  3. #define WQ_FLAG_EXCLUSIVE 0x01

  4. *private;

  5. ;

  6. ;

  7. };

  8. typedef struct __wait_queue wait_queue_t;

等待队列链表中的每个元素代表一个睡眠进程。有两种睡眠进程:互斥进程由内核有选择地唤醒,非互斥进程总是有内核在事件发生时唤醒。

③等待队列操作

DECLARE_WAIT_QUEUE_HEAD(name)init_waitqueue_head()add_wait_queue()add_wait_queue_exclusive()

此处)折叠或打开


  1. remove_wait_queue()从等待队列链表中删除一个进程

  2. wait_queue_active()函数检查一个给定的等待队列是否为空。

等待队列的详细应用参看:​​http://blog.chinaunix.net/uid-24708340-id-3070724.html​

(8)进程资源限制

每个进程都有一组资源限制(resource limit),限制制定了进程能使用的系统资源数量。其中一些可以用getrlimit和setrlimit函数查询和更改。

进程的资源限制通常是在系统初始化时由进程0建立的,然后由每个后续进程继承。

对当前进程的资源限制存放在current->signal-rlim

此处)折叠或打开


  1. struct rlimit {

  2. ; //soft limit:current limix

  3. ;//hard limit:maximum value for rlimi_cur

  4. };

更改资源时遵循三个原则

①②③常量RLIM_INFINITYRLIMIT_AS:进程可用存储区的最大长度(byte),这会影响sbrk,mmapRLIMIT_CORE:core文件的最大字节数,其值为0core

RLIMIT_CPU:CPU时间的最大量值(),当超过该值时,向该进程发送SIGXCPU信号,如果进程还不终止,再发一个SIGKILL信号。

RLIMIT_DATA:对大小的最大值(字节)RLIMIT_FSIZE:文件大小的最大值(字节)SIGXFSZRLIMIT_LOCKS:一个进程可以拥有的锁的最大数。

RLIMIT_MSGQUEUERLIMIT_NOFILE:每个进程的最大文件数。

RLIMIT_SIGPENDINGRLIMIT_STACK:栈大小的最大值。

 

3.进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,这种行为称为进程切换(process switch)、任务切换(task switch)或上下文切换(context switch).

(1) 硬件上下文

进程恢复执行前必须装入寄存器的一组数据,称为硬件上下文(hardware context),硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息,在Linux中,进程硬件上下文的一部分存放在TSS段,剩余部分存放在内核态堆栈中。

(2) 任务状态段(Task State Segment,TSS)

用来存放硬件上下文,尽管LinuxCPUTSS①当80x86CPU从用户态切换到内核态时,它从TSS中获取内核态堆栈的地址。

②当用户态进程试图通过inout指令访问一个I/O端口时,CPUTSSI/O许可权位图(Permission Bitmap)以检查该进程是否有访问端口的权利。

 tss_struct结构描述TSS的格式,每个TSS8字节的任务状态段描述符(Task State Segment Descriptor,TSSD).在intel的原始设计中,系统中的每个进程都应当指向自己的TSS,但在LinuxCPUTSS(3)thread字段

每次进程切换时,被替换进程的硬件上下文不能像Intel原始设计那样保存在TSSthread_structthreadCPU寄存器,但不包括诸如eax,ebx(4)执行进程切换

进程切换可能只发生在精心定义的点:schedule()函数,从本质上说,每个进程切换由两部组成:

①切换页全局目录以安装一个新的地址空间

②切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包括CPU寄存器。

进程切换的第二步由switch_toprev,nextlast。Prev,next是局部变量的占位符。last是输出参数,C4.创建进程

现代Unix①②()③vfork()(1) clone()、fork()及vfork()系统调用

Linux中,轻量级进程是由名为clone()的函数创建,这个函数使用下列参数:

fn:指定一个由新进程执行的函数,当这个函数返回时,子进程终止,函数返回一个整数,表示子进程的退出代码。

arg:指向传递给fn()的参数

flags:各种各样信息,低字节指定子进程结束时发送到父进程的信号代码,一般是SIGCHLD信号,剩余三个字节给一clone标志组用于编码

child_stack:表示把用户态堆栈指针赋给子进程的esp寄存器,调用进程(clone())应该总是为子进程分配新的堆栈

tls:线程局部存储段(TLS)数据结构的地址,该结构是为新轻量级进程定义的。只有CLONE_SETTLS被设置时才有意义。

ptid:父进程的用户态变量地址,该父进程具有与新轻量级进程相同的PID。

ctid:新轻量级进程的用户态变量地址,该进程具有这一类进程的PID。

clone标志

CLONE_VM:共享内存描述符和所有的页表

CLONE_FILES:共享打开文件表

实际上,clone()C库中定义的一个封装函数,它负责建立新轻量级进程的堆栈并且调用对编程者隐藏的clone()系统调用,实现clone()系统调用的sys_clone()服务例程没有fn和arg.

此处)折叠或打开


  1. asmlinkage int sys_clone(unsigned long clone_flags, unsigned long newsp,

  2. int __user *parent_tidptr, int tls_val,

  3. int __user *child_tidptr, struct pt_regs *regs)

  4. {
  5. if (!newsp)

  6. = regs->ARM_sp;

  7. (clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
  8. }

实际上,封装函数把fnargfnCPUfn(arg)

传统的fork()Linuxclone()clone()flags参数指定为SIGCHLD信号已经所有请0的clonechild_stackvfork()也是用clone()实现的,其中clone()参数flags指定为SIGCHLDCLONE_VMCLONE_VFORKclone()child_stack(2) do_fork()函数

clone(),fork()vfork()do_fork()do_fork()利用辅助函数copy_precess()来创建进程描述符以及子进程执行所需要的所有其他内核数据结构,do_fork()主要执行步骤:

①查找pidmap_arrayPID

②检查父进程的ptrace(current->ptrace)③调用copy_precess()task_struct④如果设置了CLONE_STOPPEDP->ptracePT_PTRACED⑤如果没有设置CLONE_STOPPEDwake_up_new_task()⑥如果CLONE_STOPPEDTASK_STOPPED⑦如果父进程被跟踪,则把子进程的PID存入currentptrace_messageptrace_notify().

⑧如果设置了CLONE_VFORK⑨结束并返回子进程的PID(3)copy_process()函数

copy_process()(4)内核线程(kernel thread)

内核线程不受不必要的用户态上下文拖累,只运行在内核态,只适用大于PAGE_OFFSET的线性空间。

kernel_thread()(fn),fn(arg),clone(flags),do_fork().

do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, pregs, 0, NULL, NULL);

CLONE_VM标志避免复制调用进程的页表(内核线程不会访问用户态地址空间)

CLONE_UNTRACEDpregs表示内核栈的地址。

新的内核线程开始执行fn(arg)_exit()fn()(5)进程0

Idle进程是所有进程的祖先,也叫swapper进程,是在Linux初始化阶段创建的一个内核线程。

start_kernel()1initkernel_thread(init, NULL, CLONE_FS|CLONE_SIGHAND);

内核线程10共享每进程所有的内核数据结构。

创建init0cpu_idle()hltTASK_RUNNING0.CPU0

 (6)进程1

由idleinit()init()initinitinit(7)其他的一些内核线程,一些是在初始化阶段创建Keventd:执行keventd_wq工作队列

Kapmd:处理与高级电源管理(APM)相关的时间

Pdflush:刷新“脏”缓冲区的内容到磁盘以回收内存

Kblockd:执行kblockd_workqueue工作队列中的函数,实质上周期性地激活块设备驱动程序。

Ksoftirqd:运行tasklet,系统中每个CPU都有一个这样的线程。

 

5.撤销进程

进程终止了它们本该执行的代码,即进程“死”了时,必须通知内核释放进程所拥有的资源,包括内存、打开文件及信号量等。

(1) 进程终止

进程终止一般方式是调用exit()Cexit()Cexit()main()exit_group()系统调用,终止整个线程组,由do_group_exit()exit()终止某一个进程,由内核函数do_exit()实现。Pthread_exit()线程库的系统调用

(2) do_group_exit()函数

杀死currentexit_group()()

(3)do_exit()函数

所有进程的终止都是由do_exit()(4)进程删除

Unix内核不允许进程一终止就丢弃包含在进程描述符字段中的数据,只有父进程发出了与被终止进程相关的wait()类系统调用后,才允许这样。

若父进程在子进程结束前就挂了,或者子进程死掉时父进程没有等待(即没有wait()),子进程就成为僵死进程,僵死进程将成为init的子进程,initwait()管理员在2009年8月13日编辑了该文章文章。 -->

阅读(2498) | 评论(0) | 转发(15) |