Chinaunix首页 | 论坛 | 博客
  • 博客访问: 85155
  • 博文数量: 11
  • 博客积分: 386
  • 博客等级: 一等列兵
  • 技术积分: 240
  • 用 户 组: 普通用户
  • 注册时间: 2011-12-02 17:11
文章分类

全部博文(11)

文章存档

2012年(11)

我的朋友

分类: LINUX

2012-09-15 15:41:28

进程

一个process是一个正在runningprogram,不仅包括了代码(text段),还包括各种资源(地址空间、信号量、文件描述符)。譬如,两个process可以执行同一个programprogram是一个可执行文件(elf格式等)。

Linuxtaskprocess是一个意思。Linux中不特别区分threadprocess,但thread可以看成是一种特殊的process,多个thread共享了同样的资源。

 

fork()

process通过fork()来创建,通过exec()来执行program

fork()内部调用clone()系统调用,其流程通常是分配内核栈,拷贝parent processtask_struct等副本,修改一些需要私有的变量(如PIDPPIDstatus等),然后根据flag来拷贝parent process的资源,资源包括了打开的文件描述符、信号、地址空间、page table等。来看一下clone()系统调用,普通创建一个processfork()调用clone()的方式为:

clone(SIGCHLD, 0);//只共享了signal handlers

而创建一个thread调用clone()的方式为:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

//共享了地址空间(Virtual Memory)、文件系统、文件描述符等

从中也可以看到threadsprocess的创建都靠clone(),只是flag不一样而已,因为创建child process有可能是为了执行新的program,那么在fork()复制parent process的资源就会多此一举。

fork()为了避免copy采用了Copy-on-Write技术,child process如果需要修改资源,才会拷贝一份供自己专门使用。

内核线程和普通process不一样的地方在于,它们没有地址空间(mm指针为NULL),而且只存在于内核空间,像ksoftirqdflush等都是内核线程。内核线程通过kthread_createkthread_run等来创建和运行。

 

parentchild process都从fork()返回处继续或开始执行。

child parent要执行一个新的program,它要执行exec()exec()创建一个新的地址空间并加载新的programprocess的地址空间中去执行。process通过exit()来结束。

parent process可通过wait()等函数获取child process的终止状态(zombie等)。

 

process在执行main函数中的exit()或者遇到了处理不了的signal的时候都会被终止,其流程为:

释放相关的资源(比如地址空间、从wait queue中移除、一些参考计数递减等)

通知parent process,并设置task_struct中的exit_state变量为EXIT_ZOMBIE

do_exit()调用schedule()来调度新的process。至此,该process已不能运行了,因为很多资源都释放了,但是还占有一定内存(内核栈、task_structthread_info)。需要ZOMBIE状态的原因是让parent process得知该进程退出时的状态。

接着是由parent process来清除处于ZOMBIE状态的process的所有内存。parent process通常会调用wait(),它会使parent process挂起直到收到某一个child process退出的信号。

若是parent processchild process先退出,然后child process再退出,那么kernel会重新设置child processparent(比如设置为init),否则处于ZOMBIE状态的process的资源无法完全释放。

 

task_struct结构

kernel保存了一个task list,其中的每一个元素都是一个process描述符,它包含了一个process的所有信息(地址空间、信号handlerprocess状态等),其数据结构为task_struct

task_struct是通过slab allocator来分配的。在2.6以前,task_struct是保存在每一个process的内核栈的底端。在2.6以后,一个新的结构体stuct thread_info保存在了每一个process的底端,并通过它来寻找process描述符——task_struct

通过current宏可以获取当前tasktask_structcurrent宏的原理是将内核栈指针低位清零,因为内核栈总是固定的大小(4KB或者8KB),找到栈顶的thread_struct之后再找到task_struct

一个process有一个parent process和一些child processes,遍历方法如下:


  1. 找parent process:
  2. struct task_struct *my_parent = current->parent;

  3. 找child process:
  4. struct task_struct *task;
  5. struct list_head *list;
  6. list_for_each(list, &current->children) {
  7.   task = list_entry(list, struct task_struct, sibling);
  8.   /* task now points to one of current’s children */
  9. }

  10. 找init_task
  11. struct task_struct *task;
  12. for (task = current; task != &init_task; task = task->parent)
  13.   ;

  14. process描述符通过双向链表连接在一起,所以也可以这样遍历:
  15. list_entry(task->tasks.next, struct task_struct, tasks)
  16. list_entry(task->tasks.prev, struct task_struct, tasks)

current宏只能在内核中使用吧,在用户空间不能够使用。

 

process状态

process有这么几种状态:

TASK_RUNNING——process正在running或在running queue中等待着。

TASK_INTERRUPTIBLE——process正在sleepingblocked),当条件满足,kernel会设置process的状态为TASK_RUNNING,从而唤醒这个process。处于该状态的process也可能因为收到signal而被唤醒。

TASK_UNINTERRUPTIBLE——与TASK_INTERRUPTIBLE区别在于即使收到signal也不会被唤醒。

还有__TASK_TRACED__TASK_STOPPED

通常process运行在用户空间,通过系统调用进入内核空间。此时的内核便处于进程上下文(process context),也可以说是内核代表着process在执行。退出内核空间的时候,若有高优先级的process,可能会发生process的切换。

另一个与process context相对应的是中断上下文(interrupt context),此时内核不代替process来执行,而是执行一个中断处理程序(interrupt handler)。处于中断上下文时,不存在process的概念(No process is tied to interrupt handlers),因为它没有task_struct

 

进程调度

通过进程调度(process scheduling)让人感觉所有的process都是同时运行的。

进程调度有两个指标:低延迟fast process response time (low latency) 和高吞吐量 maximal system utilization (high throughput)。像IO型的process(硬盘、GUIkeyboard、网络)等都是访问频率高但是计算量小的,而像算法等process是访问频率低但是消耗计算量的,所以对于后者需要尽力减小scheduling的次数来提高效率。

Linux采用抢占式的进程调度,涉及优先级(prioritynice值)和时间片(time slice)两个概念。Linux的调度算法称为Completely Fair Scheduler (CFS)CFS算法不分配给每个process一个固定的timeslice,而是根据nice值计算每个process在一个target latency中的权重,或者说是在一个target latency能够占用CPU的时间比重。当某个process的时间用尽,下一个被调度的process应该也是通过这两个值确定的吧?

系统定时器(system timer)以HZ的频率发出中断,中断间隔为一个tick。在该中断的处理程序中会减少当前process可以用的时间片,这些信息保存在task_structstruct sched_entity变量中。

 

进程的sleep/阻塞/挂起

引入sleep是因为很多时候process需要等待某些事件,如果没有sleep的话,CPU只能循环忙等了。sleep的流程为:

标记process的状态为sleepTASK_INTERRUPTIBLEUNINTERRUPTIBLE

加入waiting queue

退出running queue

调用schedule()(其实是交出CPU

当条件满足,kernel会唤醒在wait queue上的process(全部或单个)。

 

抢占

抢占分为用户抢占(user preemption)和内核抢占(kernel preemption)。

用户抢占可能会发生在:系统调用返回至用户空间,中断处理返回至用户空间,进程sleep,显示调用schedule()等情况。

返回的时候会检查process中的need_resched标志(系统时间中断设置的?preempt_count?),如果制位,说明有高优先级的process准备中,这样就会schedule()

以前内核是不能被抢占的,处于内核空间的process一定是等执行完其在内核空间的代码才能够调度。2.6之后内核可以被抢占。

 

系统调用

POSIX提供了标准的API接口,C库实现了API函数包括系统调用的接口。

通常C库函数的返回值为0表述函数执行成功,非0表示失败,并记录在全局变量errno中,通过perror()函数可以打印出具有可读性的错误信息。

系统调用的函数通常都是以sys_xxx()命名。考虑到安全性,应用程序不能直接访问内核空间,所以系统调用可以看成是应用程序通知内核去执行某一段内核程序,也可以看成为内核代替应用程序在执行。而应用程序通知内核的方法就是软件中断(不是软中断),在X86上就是“int $0x80”指令,因为软件中断号是128,在触发软件中断以前,需要设置系统调用号(syscall number)和系统调用参数。

仍然出于安全性的考虑,从用户空间传递给内核空间的读写指针需经过检查,这通过copy_from_usercopy_to_user来完成。这两个函数都有可能会导致挂起,因为需操作的用户数据可能在swap区域,而swap至内存是会让process sleep的。

执行系统调用的时候,内核处于进程上下文(process context),它是可以sleep、调度和被抢占的。

 

进程地址空间

task_struct中包含的mm_structvm_area_struct描述了上述地址空间的布局。

进程在用户空间使用的是用户空间的堆栈,进入内核态后,使用内核空间的堆栈。

进入内核态时,需要保存进程在用户态的运行环境,如被打断的位置、CPU现场信息(如寄存器、用户栈地址)等;恢复用户态时,从内核栈上保存的内容中恢复用户态的运行环境即可。

内核栈顶地址信息保存在task_struct中,而且进程从用户态转到内核态的时候,进程的内核栈总是空的,所以用户态就不用保存内核栈地址。

 

栈布局

图片来源:http://blog.csdn.net/wangxiaolong_china/article/details/6844371,感谢作者。

 

参考

Linux Device Drivers

Linux Kernel Development

http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory

http://blog.chinaunix.net/uid-20543672-id-2996319.html

http://blog.csdn.net/wangxiaolong_china/article/details/6844371

阅读(1738) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~