进程是程序执行时的一个实例。从内核观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的实体
进程描述符
为了管理进程,内核定义进程描述符(process descriptor)用于描述进程的各种属性。进程描述符都是task_struct类型结构。
在linux内核中task_struct拥有上百的成员变量,是一个相当复杂的结构,它不仅包含了很多进程属性的字段,而且一些字段还包括了指向其他数据结构的指针。下面的代码仅仅列举几个成员:
struct task_struct{ volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ void *stack; atomic_t usage; unsigned int flags; /* per process flags, defined below */ unsigned int ptrace; ... }
进程状态
进程描述符中的state字段描述了进程当前所处的状态。它由一组标志组成,其中每个标志描述描述一种可能的进程状态。
可运行状态(TASK_RUNNING)
进程要么在CPU上执行,要么准备执行
可中断的等待状态(TASK_INTERRUPTIBLE)
进程被挂起(睡眠),直到某个条件变为真。产生一个硬件中断,释放进程正等待的系统资源,或传递一个信号都是可以唤醒进程的条件(把进程的状态放回到TASK_RUNNING)
不可中断的等待状态(TASK_UNINTERRUPTIBLE)
与可中断的等待状态类似,但有一个例外,把信号传递睡眠进程不能改变它的状态。这种状态很少用到,但在一些特定的情况下,这种状态是很有用的。例如,当进程打开一个设备文件,其相应的设备驱动程序开始探测相应的硬件设备时会用到这种状态。探测完成以前,设备驱动程序不能被中断,否则,硬件设备会处于不可预知的状态。
暂停状态(TASK_STOPPED)
进程的执行被暂停。当进程接收到SIGSTOP、SIGSTP、SIGTTIN或SIGTTOU信号后,进入暂停状态。
跟踪状态(TASK_TRACED)
进程的执行已由debugger程序暂停。当一个进程被另一个进程监控时,任何信号都可以把这个进程置于TASK_TRACED状态。
另外有两个进程状态是既可以存放在进程描述符的state字段中,也可以存放在exit_state字段中。
僵死状态(EXIT_ZOMBIE)
进程的执行被终止,但是,父进程还没有发布wait4()或waitpid()系统调用来返回有关死亡进程的信息。发布wait类系统调用前,内核不能丢弃包含在死进程描述符中的数据,因为父进程可能还需要它。
僵死撤销状态(EXIT_DEAD)
最终状态:由于父进程刚发出wait4()或waitpid()系统调用,因而进程由系统删除。
内核使用set_task_state和set_current_state宏来分别设置指定进程的状态和当前执行进程的状态。
#define mb() asm volatile ("": : :"memory") #define set_mb(var, value) do { var = value; mb(); } while (0) #define __set_task_state(tsk, state_value) do { (tsk)->state = (state_value); } while (0) #define set_task_state(tsk, state_value) set_mb((tsk)->state, (state_value)) /* * set_current_state() includes a barrier so that the write of current->state * is correctly serialised wrt the caller‘s subsequent test of whether to * actually sleep: * * set_current_state(TASK_UNINTERRUPTIBLE); * if (do_i_need_to_sleep()) * schedule(); * * If the caller does not need such serialisation then use __set_current_state() */ #define __set_current_state(state_value) do { current->state = (state_value); } while (0) #define set_current_state(state_value) set_mb(current->state, (state_value))
正如注释描述的那样,set_task_state、set_current_state含有内存屏障,它们是严格执行串行化指令,编译器不会进行任何优化
__asm__用于指示编译器在此插入汇编语句,__volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。
memory 强制gcc 编译器假设RAM 所有内存单元均被汇编指令修改,这样cpu 中的registers 和cache 中已缓存的内存单元中的数据将作废。cpu 将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu 又将registers,cache 中的数据用于去优化指令,而避免去访问内存。
一般来说,能被独立调度的每个执行上下文都必须拥有它自己的进程描述符;因此,即使共享内核大部分数据结构的轻量级进程,也有它们自己的task_struct结构。
一个线程组中的所有线程使用和该线程组的领头线程(thread group leader)相同的PID,也就是该组中第一个轻量级进程的PID,它被存入进程描述符的tgid字段中。getpid()系统调用返回当前进程的tgid值而不是pid的值,因此,一个多线程应用的所有线程共享相同的PID。
对每个进程来说,Linux都把两个不同的数据结构紧凑地放在一个单独为进程分配的存储区域:一个是内核的进程堆栈,另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符,这块存储区的大小通常为8192个字节(两个页框)。考虑到效率的因素,内核让这8K空间占据连续的两个页框并让第一个页框的起始地址是2^13的倍数。
C语言使用下列的联合结构方便地表示一个进程的线程描述符和内核栈:
union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; };
进程间的关系
程序创建的进程具有父子关系。如果进程创建多个子进程时,则子进程具有兄弟关系。在进程描述符中引入几个字段来表示这些关系。
real_parent 指向创建了P的进程描述符,如果P的父进程不再存在,就指向进程1(init)的描述符(因此,如果用户运行一个后台进程而且推出了 shell,后台进程就会成为init的子进程)
parent 指向P的当前父进程(这种进程的子进程终止时,必须向父进程发信号)。它的值通常与real_parent一致。
children 链表的头部,链表中的所有元素都是P创建的子进程
sibling 指向兄弟进程链表中的下一个元素或前一个元素的指针,这些兄弟进程的父进程都是P
group_leader P所在进程组的领头进程的描述符指针
tgid P所在线程组的领头进程的PID
ptraced 该链表包含所有被debugger程序跟踪的P的子进程
ptrace_entry 指向所跟踪进程其实际父进程链表的前一个或者下一个元素(用于P被跟踪的时候)
/include/linux/sched.h
struct task_struct *real_parent; /* real parent process */ struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */ /* * children/sibling forms the list of my natural children */ 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 */ /* * ptraced is the list of tasks this task is using ptrace on. * This includes both natural children and PTRACE_ATTACH targets. * p->ptrace_entry is p‘s link on the p->parent->ptraced list. */ struct list_head ptraced; struct list_head ptrace_entry;
等待队列
等待队列在内核中有很多用途,尤其用在中断处理、进程同步及定时。如,等待一个磁盘操作的终止,等待释放系统资源,或等待时间经过固定的时间间隔。等待队列实现了在事件上的条件等待:希望等待特定事件的进程把自己放在合适的等待队列,并放弃控制权。因此,等待队列代表一组睡眠的进程,当某条件变为真时,由内核唤醒它们。
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; }; struct __wait_queue_head { spinlock_t lock; struct list_head task_list; };
因为等待队列是由中断处理程序和主要内核函数修改的,因此必须对其双向链表进行保护,以免对其访问的同时会导致不可预测的后果。同步时通过等待队列头中的lock自旋锁达到的。
等待队列链表中的每个元素代表一个睡眠进程。该进程等待某一事件的发生;然而,要唤醒等待队列中所有睡眠的进程有时并不方便。例如,如果两个或多个进程在等待互斥访问某一要释放的资源,仅唤醒等待队列的一个进程才有意义。这个进程占有资源,而其他进程继续睡眠。因此,有两种睡眠进程:互斥进程(等待队列元素的flags字段为1)由内核有选择的唤醒,而非互斥进程(flags值为0)总是由内核在事件发生时唤醒。
要等待特定条件的进程可以调用如下列表中的任何一个函数
1、sleep_on()该函数把当前进程的状态设置为TASK_UNINTERRUPTIBLE,并把它插入到特定的等待队列。然后调用调度程序,而调度程序重新开始另外一个程序的执行。当睡眠进程被唤醒时,调度程序重新开始执行sleep_on()函数,把该进程从等待队列中删除。
2、interruptible_sleep_on()与sleep_on()函数是一样的,但稍有不同,前者把当前进程状态设置为TASK_INTERRUPTIBLE而不是TASK_UNINTERRUPTIBLE,因此,接收一个信号可以唤醒当前进程。
3、sleep_on_timeout()和interruptible_sleep_on_timeout()与前面函数类似,但它允许调用者定义一个时间间隔,过了这个间隔以后,进程有内核唤醒。为了做到这一点,它们调用schedule_timeout()函数而不是schedule();
4、函数prepare_wait()和prepare_to_wait_exclusive()用传递的第三个参数进程的状态,然后把等待队列元素的互斥标志flag分别设置为0或1,最后,把等待元素wait插入到以wq为头的等待队列的链表中。进程一旦被唤醒就执行finish_wait()函数,它把进程的状态再次设置为TASK_RUNNING,并从等待队列中删除等待元素。
5、wait_event和wait_event_interruptible宏使它们的调用进程在等待队列上睡眠,一直到修改了给定条件为止。
sleep_on()函数在以下条件不能使用,那就是必须测试条件并且当条件还没有得到验证时又紧接着让进程去睡眠;此外,为了把一个互斥进程插入等待队列,内核必须用prepare_wait_exclusive()函数。所有其他相关函数把进程当作非互斥进程来插入。
进程(一)