看的第二天,还是要好好学习下操作系统相关的,为后面写驱动做准备。
进程
进程是任何多道程序设计的操作系统中的基本概念。通常把进程定义为程序执行的一个实例。
该节将首先介绍进程的静态特性,然后描述内核如何进行进程切换。
进程、轻量级进程和线程
进程是程序执行时的一个实例。你可以把它看作充分描述程序以及执行到何种程度的数据结构的汇集。
从内核观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的实体。
当一个进程创建时,它几乎与父进程相同。它接受父进程地址空间的一个(逻辑)拷贝。并从进程创建系统调用的下一条指令开始执行与父进程相同的代码。尽管父子进程可以共享含有程序代码(正文)的页,但是它们各自有独立的数据拷贝(堆和栈),因此子进程对一个内存单元的修改对父进程是不可见的(反之亦然)。
现代Unix系统并没有使用上面的简单模式。它们支持多线程应用程序----拥有很多相对独立执行流的用户程序共享应用程序的大部分数据结构。在这样的系统中,一个进程由几个用户线程组成,每个线程都代表进程的执行流。
Linux使用轻量级进程对多线程应用程序提供更好的支持。两个轻量级进程基本上可以共享一些资源,诸如地址空间、打开的文件等等。只要其中一个修改共享资源,另一个就立即查看这种修改。当然,当两个线程访问共享资源时就必须同步他们自己。
实现多线程应用程序的一个简单方式就是把轻量级进程与每个线程关联起来。
进程描述符
为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。例如,内核必须知道进程的优先级,它是正在CPU上运行还是因某些原因而被阻塞,给它分配了什么样的地址空间,允许它访问哪些文件等。
进程描述符都是task_struct类型结构,它的字段包含了与一个进程相关的所有信息。因此进程描述符中存放了那么多信息,所以它是相当复杂的。它不仅包含了很多进程属性字段,而且一些字段还包括了指向其他数据结构的指针。
本阶段集中讨论进程的状态和进程的父子关系。
进程状态
顾名思义,进程描述符中的state字段描述了进程当前所处的状态。它由一组标志组成,其中每个标志描述一种可能的进程状态。在当前的Linux版本中,这些状态是互斥的,因此,严格意义上说,只有设置一种状态;其余的标志将被清除。下面是进程可能的状态:
可运行状态(TASK_RUNNING)
进程要么在CPU上执行,要么准备执行。
可中断的等待标志(TASK_INTERRUPTIBLE)
进程被挂起(睡眠),直到某个条件变为真。产生一个硬件中断,释放进程正在等待的系统资源,或传递一个信号都可以是唤醒进程的条件(把进程的状态放到TASK_RUNNING)
不可中断的等待状态(TASK_UNINTERRUPTIBLE)
与可中断的等待状态类似,但有一个例外,把信号传递到睡眠进程不能改变它的状态。
暂停状态(TASK_STOPPED)
进程的执行被暂停。当进程接收到SIGSTOP、SIGTSTP、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宏:他们分别设置指定进程的状态和当前执行进程的状态。
标识一个进程
一般来说能被独立调度的每个执行上下文都必须拥有它自己的进程描述符;因此,即使共享内核大部分数据结构的轻量级进程,也有它们自己的task_struct结构。
进程和进程描述符之间有非常严格的一一对应关系,这使得用32位进程描述符地址标识进程称为一种方便的方式。进程描述符指针指向这些地址,内核对进程的大部分引用是通过进程描述符指针进行的。
进程描述符处理
进程是动态实体,其生命周期范围从几毫秒到几个月。因此内核必须能够同时处理很多进程,并把进程描述符存放在动态内存中,而不是放在永久分配给内核的内存区中。
对每个进程来说,Linux都把两个不同的数据结构紧凑地存放在一个单独为进程分配的内存区域:
一个是内核态的进程堆栈;
另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。
内核通过使用alloc_thread_info和free_thread_info宏分配和释放存储thread_info结构和内核栈的内存区。
标识当前进程
从效率的观点来看,刚才所讲的thread_info结构的内核态堆栈之间的紧密结合提供的主要好处是:
内核很容易从esp寄存器的值获得当前在CPU上正在运行进程thread_info结构的地址。
进程最常用的是进程描述符的地址而不是thread_info结构的地址。为了获得当前在CPU上运行进程的描述符指针,内核要调用current宏,该宏本质上等价于current_thread_info()->task。
双向链表
对每个链表,必须实现一组原语操作:初始化链表,插入和删除一个元素,扫描链表等等。
新链表是用LIST_HEAD(list_name)宏创建的。它声明类型为list_head的新变量list_name,改变了作为一个新链表的占位符,是一个哑元素。LIST_HEAD(list_name)宏还初始化list_head数据结构的prev和next字段,让它们指向list_name变量本身。
有以下实现原语的函数和宏。
list_add(n,p) 把n指向的元素插入p所指向的特定元素之后
list_add_tail(n,p) 把n指向的元素插入p所指向的特定元素之前
list_del(p) 删除p指向的元素
list_empty(p) 检查由第一个元素的地址p指定的链表是否为空
list_entry(p,t,m) 返回类型为t的数据结构的地址,其中类型t中含有list_head字段,而list_head字段中含有名字m和地址p
list_for_each(p,h) 对表头地址h指定的链表进行扫描,在每次循环时,通过p返回指向链表元素的list_head结构的指针。
list_for_each_entry(p,h,m) 与上类型,但是返回包含了list_head结构的数据结构的地址,而不是list_head结构本身的地址。
进程链表
进程链表把所有进程的描述符链接起来。每个task_struct结构都包含一个list_head类型的tasks,这个类型的prev和next字段分别指向前面和后面的task_struct元素。
进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符。init_task的tasks.prev字段指向链表中最后插入的进程描述符的tasks字段。
SET_LINKS和REMOVE_LINKS宏分别用于从进程链表中插入和删除一个进程描述符。
for_each_process,它的功能是扫描整个进程链表。
TASK_RUNNING状态的进程链表
当内核寻找一个新进程在CPU上运行时,必须只考虑可运行进程(即处于TASK_RUNNING状态的进程)。
内核必须为系统中每个运行队列保存大量的数据,不过运行队列的主要数据结构还是组成运行队列的进程描述符链表,所有这些链表都由一个单独的prio_array_t数据结构来实现。管理140个优先权队列的头结点。
enqueue_task(p,array)函数把进程描述符插入某个运行队列的链表。
dequeue_task(p,array)函数把进程描述符删除一个运行队列的链表。
进程间的关系
程序创建的进程具有父/子关系,如果一个进程创建多个子进程时,则子进程之间存在兄弟关系。
pidhash表及链表
顺序扫描进程链表并检查进程扫描符的pid字段是可行但是相当低效的。为了加速查找、引入了4个散列表。需要4个散列表是因为进程描述符包含了表示不同类型PID的字段,而且没种类型的PID需要它自己的散列表。
4个散列表和进程描述符中的相关字段
Hash表的类型 字段名 说明
PIDTYPE_PID pid 进程的PID
PIDTYPE_TGID tgid 线程组领头进程的PID
PIDTYPE_PGID pgrp 进程组领头进程的PID
PIDTYPE_SID session 会话领头进程的PID
内核初始化的时候,动态的为4个散列表分配空间,并把它们的地址存入pid_hash数组。
用pid_hashfn宏把PID转化为表索引,pidhash_shift用来存放表索引的长度。
如何组织进程
运行队列链表把处于TASK_RUNNING状态的所有进程组织到一起。当要把其他状态的进程分组时,不同的状态要求不同的处理,Linux选择了下列方式之一:
没有处于TASK_STOPPED、EXIT_ZOMBIE或EXIT_DEAD状态的进程建立专门的链表。由于对处于暂停、僵死、死亡状态的进程的访问比较简单,或者通过PID、或者通过特定父进程的子进程链表,所以不必对着三种状态进行分组。
没有为处于、状态的进程建立专门的链表。
等待队列
等待队列在内核中有很多用途,尤其用在中断处理、进程同步及定时。等待队列实现了在事件上的条件等待:希望等待待定事件的进程把自己放进合适的等待队列,并放弃控制权。
因为等待队列是由中断处理程序和主要内核函数修改的,因此必须对其双向链表进行保护以免对齐进行同事访问,因为同事访问会导致不可预测的后果。同步是通过等待队列头中的lock自旋锁达到的。
等待队列链表中的每个元素代表一个睡眠进程,该进程等待某一事件的发生;他的描述符存放在task字段中。
有两种睡眠进程:互斥进程(等待队列元素的flags字段为1)由内核有选择地唤醒,而非互斥进程(flags值为0)总是由内核在事件发生时唤醒。.
等待队列操作
可以用DECLARE_WAIT_QUEUE_HEAD(name)宏定义一个新等待队列的头,它静态地声明一个叫name的等待队列的头变量并对该变量的lock和task_list字段进行初始化。
函数init_waitqueue_head()可以用来初始化动态分配的等待队列头变量。
一旦定义了一个元素,必须把它插入等待队列。
add_wait_queue()函数把一个互斥进程插入等待队列链表的第一个位置。
add_wait_queue_execlusive()函数把一个互斥进程插入等待队列链表的最后一个位置。
remove_wait_queue()函数从等待队列链表中删除一个进程。
waitqueue_active()函数检查一个给定的等待队列是否为空。
进程资源限制
每个进程都有一组相关的资源限制(resource limit),限定制定了进程能使用的系统资源数量。这些限制避免用户过分使用系统资源(CPU、磁盘空间等)。
对当前进程的资源限制存放在current->signal->rlim字段,即进程的信号描述符的一个字段。
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并回复以前挂起的某个进程的执行。这种行为被称为进程切换、任务切换、或上下文切换。
硬件上下文
尽管每个进程可以拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此在恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程的值。
进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。在Linux中,进程硬件上下文的一部分存放在TSS段,而剩余部分存放在内核态的堆栈中。
thread字段
在每次进程切换时,被替换进程的硬件上下文必须保存在别处。因为Linux为每个CPU而不是每个进程使用TSS。
执行进程切换
进程切换可能只发生在精心定义的店:schedule()函数。这里我们只关注内核如何执行一个进程的切换。
从本质上说每个进程的切换分为两步组成:
1、切换页全局目录以安装一个新的地址空间;
2、切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。
进程切换的第二步由switch_to宏执行。它是内核中与硬件关系最密切的例程之一。
switch_to(prev,next,last):
prev、next分别表示被替换进程和新进程描述符的地址在内存中的位置。
last是输出参数,它表示宏把进程C的描述符地址卸载内存的什么位置了。
保存和加载FPU、MMX及XMM寄存器
从80486DX开始,算数浮点单元(FPU)已被集成到CPU中。
创建进程
Unix操作系统仅仅依赖进程创建来满足用户需求。
传统的Unix操作系统以同意的方式对待所有的进程:子进程复制父进程拥有的资源。这种方法是进程的创建非常慢且效率非常低,因为子进程需要拷贝父进程的整个地址空间。实际上子进程几乎不必读或者修改父进程拥有的所有资源,在很多情况下,子进程立即调用execve,并清除父进程仔细拷贝过来的地址空间。
现代Unix内核通过引入三种不同的机制解决了这个问题:
1、写时复制技术允许父子进程读相同的物理页。
2、轻量级进程允许父子进程共享每个进程内核的很多数据结构,如页表、打开文件表及信号处理。
3、vfork()系统调用创建的进程能共享其父进程的内存地址空间。
clone(),fork(),vfork()系统调用
在Linux中,轻量级进程是由名为clone()的函数创建的。
fork()、vfork()系统调用在Linux中都是用clone()实现的。
do_fork()函数
do_fork()函数负责处理clone(),fork,vfork()系统调用。
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs, 指向通用寄存器值得指针,通用寄存器的只是在从用户态切换到内核态时被保存到内核态堆栈中的。
unsigned long stack_size, 未使用0。
int __user *parent_tidptr, 与clone()中的ptid相同
int __user *child_tidptr) 与clone()中的ctid相同
do_fork()利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的其他内核数据结构。
copy_process()函数
copy_process()创建进程描述符以及子进程执行所需的所有其他数据结构。他的参数和do_fork()相同;
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)
内核线程
传统的Unix系统吧一些重要的任务委托给周期性执行的进程,这些任务包含刷新磁盘高速缓存,交换出不用的页框,维护网络连接等等。
在Linux中,内核线程在以下几个方面不同于普通进程:
内核线程只运行在内核态,而普通进程即可以运行在内核态,也可以运行在用户态。
因为内核线程只运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间。另一方面,不管在用户态还是内核态,普通用户都可以用4GB的线性地址空间。
创建一个内核线程
kernel_thread()函数创建一个新的内核线程,它接受的参数有:所要执行的内核函数的地址(fn)、要传递给函数的参数(arg)、一组clone标志(flags)。
进程0
所有进程的祖先叫做进程0,idle进程或因为历史的原因叫做swapper进程,它是在Linux初始化阶段从无到有创建的一个内核线程。
进程1
由进程0创建的内核线程执行init()函数,init()依次完成内核初始化。init()调用execve()系统调用装入可执行程序init。
结果init内核线程变为一个普通进程,且拥有自己的进程的内核数据结构。
在系统关闭之前,init进程一直存活,应为它创建和监控在操作系统外层执行的所有进程的活动。
其他内核线程
Linux使用很多其他内核线程。其中一些在初始化阶段创建,一直运行到系统关闭;而其他一些在内核必须执行一个任务时“按需“创建,这种任务在内核的执行上下文中得到了很好的执行。
一些内核线程的例子(除了进程0和进程1):
keventd(也被称为事件)、kapmd(处理与高级电源管理(APM)相关的事件、kswapd(执行内存回收)、pdflush(刷新“脏”缓冲区中的内容到磁盘以回收内存)。
kblockd(执行kblockd_workqueue工作队列中的函数)、ksoftirqd(运行tasklet,系统中每个CPU都有一个这样的内核线程)
撤销进程
很多进程终止了它们本该执行的代码,从某种意义上来说,这些进程死了。当这种情况发生时,必须通知内核以便内核释放进程所拥有的资源,包括内存、打开文件等。
内核可以有选择地强迫整个线程组死掉。这发生在以下两种典型情况下:
当进程接收到一个不能处理或忽略的信号时,
或者当内核正在代表进程运行时在内核态产生一个不可恢复的CPU异常时。
进程终止
在Linux 2.6中有两个终止用户态应用的系统调用:
exit_group():它终止整个线程组,即整个基于多线程的应用。do_exit_group()是这个函数调用的主要内核函数。
exit():它终止某一个线程,而不管该线程所属线程组中的所有其他进程。do_exit()是实现这个系统调用的主要内核函数。
进程删除
Unix允许进程查询内核以获得其父进程PID,或者其任何子进程的执行状态。
例如:进程可以创建一个子进程来执行特定的任务,然后调用诸如wait()这样的一些库函数检查子进程是否终止。如果子进程终止了,那么它的终止代号将告诉父进程这个任务是否已成功完成。
为了遵循这些设计选择,不允许Unix内核在进程一终止后就丢弃包含在进程描述符字段的数据。只有父进程发出与被终止的进程相关的wait()类系统调用之后,才允许这样做。
这就引入僵死状态的原因;尽管从技术上来说进程已死,但必须保存它的描述符,知道父进程得到通知。
release_task()函数从僵死进程的描述符中分离出最后的数据结构;对僵死进程的处理有两种可能的方式:
如果父进程不需要接受来自子进程的信号,就调用do_exit();在这种情况下,内存的回收将由进程调度程序来完成。
如果已经给父进程发送了一个信号,就需要调用wait4(),waitpid()系统调用。在这种情况下函数还将回收进程描述符所占的内存空间。