最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

进程

来源:博客园

进程、轻量级进程和线程

进程在教科书中通常定义:进程是程序执行时的一个实例,可以把它看作充分描述程序已经执行到何种程度的数据结构的汇集。

从内核的观点,进程的目的就是担当分配系统资源(CPU时间、内存等)的实体。

当一个进程被创建时,他几乎于父进程相同。它接受父进程地址空间的一个(逻辑)拷贝,并从进程创建系统调用的下一条指令开始执行于父进程相同的代码。尽管父子进程可以共享含有程序代码(正文)的页,但是他们各自有独立的数据拷贝(栈和堆),因此子进程对一个内存单元的修改对父进程是不可见的。


【资料图】

Unix中一个进程由几个线程组成,每个线程都代表进程的一个执行流。从内核观点来看,多线程应用程序仅仅是一个普通进程。多线程应用程序多个执行流的创建、处理、调度整个都是在用户态进行的。

Linux使用轻量级进程对多线程应用程序提供更好的支持。两个轻量级进程基本上可以共享一些资源,诸如地址空间、打开的文件等。只要其中一修改共享资源,另一个就立即查看这种修改。

实现多线程应用程序的一个简单方式就是轻量级进程与每个线程关联起来,保证共享资源的同时,每个线程都可以由内核独立调度。

进程描述符

进程描述符都是task_struct类型结构,它的字段包含了与一个进程相关的所有信息。

进程状态

进程描述符中的state字段描述了进程当前所处的状态。

可运行状态(TASK_RUNNING):进程要么在CPU执行,要么准备执行。

可中断的等待状态(TASK_INTERRUPTIBLE):进程被挂起(睡眠),直到某个条件为真。产生一个硬件中断,释放当前的进程资源,或传递一个唤醒的信号。

不可中断的等待状态(TASK_UNINTERRUPTIBLE):与可中断的等待状态类似,有个例外信号传递不能改变状态。用的很少,某些情况下(进程必须等待,直到一个不能被中断的事件发生)。例如,当进程打开一个设备文件,其相应的驱动程序开始探测相应的硬件设备时会用到,在探测完成前,设备驱动程序不能被中断,否则,硬件设备会处于不可预知的状态。

暂停状态(TASK_STOPPED):进程执行被暂停,当进程收到SIGTOP、SIGTSTP、SIGTTIN或SIGTTOU信号后,进入暂停状态。

跟踪状态(TASK_TRACED):进程的执行已由debugger程序暂停。当进程被另一个进程监控时,任何信号都可以把这个进程置于该状态。

还有两个状态既可以放在进程描述符的state字段,又可以放在exit_state字段中。当进程被终止时,进程的状态才会变成两种状态的一种:

僵死状态(EXIT_ZOMBIE):进程的执行被终止,但是父进程还没有发布wait4()或waitpid()系统调用来返回有关死亡进程的信息。发布wait()类系统调用前,内核不能丢弃包含在死进程描述符中的数据,因为父进程可能还需要它。

僵死撤销状态(EXIT_DEAD):最终状态,由于父进程刚发出wait4()或waitpid()系统调用,因而进程由系统删除。为了防止其他执行线程在同一个进程上也执行wait()类系统调用(这是一种竞争条件),而把进程的状态由僵死改为僵死撤销状态。

进程链表

采用双向链表把所有进程的描述符链接起来。

每个task_struct结构都包含一个list_head类型的tasks字段,这个类型的prev和next字段分别指向前面和后面的task_struct元素。

进程链表的头是init_task描述符,它是所谓的0进程(process0)或swapper进程的进程描述符。init_task的task.prev字段指向链表中最后插入的进程描述符的tasks字段。

TASK_RUNNING状态的进程链表

当内核寻找一个新进程在CPU上运行时,必须考虑可运行进程。

提高调度程序运行速度的诀窍时建立多个可运行的程序链表,每种进程优先权对应一个不同的链表。每个task_struct描述符包含一个list_head类型的字段run_list,用于保存可运行程序的优先级。在多处理器系统中,每个CPU都有自己的运行队列,即他自己的进程链表集。

运行队列的主要数据结构是组成运行队列的进程描述符链表,由一个单独的priio_array_t数据结构来实现。

p95图

进程间的关系

程序创建具有父子关系。如果一个进程创建多个子进程时,则子进程之间具有兄弟关系。

进程0和进程1是由内核创建的,进程1(init)是所有进程的祖先。

为了加速查找。引入了4个散列表,没中过类型的PID需要他自己的散列表。

p97图

Linux利用双向链表来处理冲突的PID,每一个表项都是由冲突的进程描述符组成的双向链表。

等待队列

等待队列由双向链表实现,其元素包括指向进程描述符的指针,等待队列头是一个类型为wait_queue_head_t的数据结构。

struct __wait_queue_head{    spinlock_t lock;    struct list_head task_list;};typedef struct __wait_queue_head wait_queue_head_t;

因为等待队列是由中断处理函数和主要内核函数修改的,因此必须对其双向链表进行保护以免对其进行访问。同步是通过等待队列头中的lock自旋锁达到。

等待队列链表中的元素类型为wait_queue_t:

struct __wait_queue{    unsigned int flags;    struct task_struct * task;    wait_queue_func_t func;    struct list_head task_list;};typedef struct __wait_queue wait_queue_t;

描述符地址存放在task字段中,task_list字段包含的是指针,由这个指针把一个元素连接到等待相同事件的进程链表中。

有两种睡眠进程:互斥进程(flags=1)由内核有选择的唤醒,而非互斥进程(flags=0)总是由内核在事件发生时唤醒。等待访问临界资源的进程就是互斥的,等待相关事件的进程时非互斥的,func字段表示唤醒的方式。

一旦定义了一个元素,必须把它插入等待队列。add_wait_queue()函数把一个非互斥进程插入等待队列链表的第一个位置,add_wait_queue_exclusive()函数把一个互斥进程插入等待队列链表的最后一个位置。remove_wait_queue()函数从等待队列链表中删除一个进程。waitqueue_active()函数检查一个给定的等待队列是否为空。

wake_up_locked宏和wake_up宏相类似,仅有的不同是当wait_queue_head_t中的自旋锁已经被持有时要调用wake_up_locked;

例如,wake_up宏等价于下列代码:

void wake_up(wait_queue_head_t *q){    struct list_head *tmp;    wait_queue_t *curr;    list_for_each(tmp,wait_queue_t,task_list){        curr = list_entry(tmp,wait_queue_t,task_list);        if(curr->func(curr, TASK_INTERRUPTIBLE|TASK_UNINTERRUPTBLE,0,NULL) && curr->flags)            break;    }}

list_for_each宏扫描双向链表中的所有项,即等待队列的所有进程。对每一项,list_entry宏都计算wait_queue_t变量对应的地址。这个变量的func字段存放唤醒函数的地址,它试图唤醒由等待队列元素的task字段标识的进程。如果一个进程已经被有效地唤醒并且进程是互斥的,循环结束。

因为所有的非互斥进程总是在双向链表的开始位置,而所有的互斥进程在双向链表的尾部,所以函数总是先唤醒非互斥进程然后再唤醒互斥进程,如果有进程存在的话。

进程切换

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

硬件上下文

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

prev--切换出的进程的描述符 next--切换进的进程的描述符

我们把进程切换定义为这样的行为:保存prev硬件上上下文,用next硬件上下文代替prev。

执行进程切换

从本质上说,每个进程切换由两部组成:

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

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

switch_to宏

进程切换的第二步由switch_to宏执行,它是内核中与硬件关系最密切的历程之一。

他有三个参数,prev、next 和 last。

在任何进程切换中涉及到三个进程而不是两个进程,假设内核决定暂停进程A而激活进程B。在shedule()函数中,prev指向A的描述符而next指向B的描述符。switch_to宏一旦使A暂停,A的执行流就冻结。随后,当内核想再次激活进程A,就必须暂停另一个进程C,于是就要用prev指向C而next指向A来执行另一个switch_to宏。

switch_to宏的最后一个参数是输出参数,它表示宏把进程C的描述符地址写在内存的什么位置了。

p110图

创建进程

clone()、fork()及 vfork()系统调用

在Linux中,轻量级进程是由名为clone()的函数创建的。

传统的fork()系统调用在Linux中是用clone()实现的,其中clone()的flags参数指定为SIGCHLD信号及所有清0的clone标志,而它的child_stack参数是父进程当前的堆栈指针。因此,父进程和子进程暂时共享一个用户态堆栈。

要感谢写时复制机制,通常只要父子进程中有一个试图去改变,则立即各自的到用户态堆栈的一份拷贝。

do_fork()函数负责处理clone()、fork()和vfork()系统调用,利用辅助函数 copy_process()来创建进程描述符以及子进程所需要的内核数据结构。

do_fork()之后有了处于可运行状态的完整子进程,但他还没有实际运行,调度程序决定何时把CPU交给这个子进程。在以后的进程切换中,调度程序将继续完善子进程。然后,在fork()、vfork()、clone()系统调用结束后,新进程将开始执行。系统调用的返回值放在eax寄存器中,返回给子进程的值是0,返回给父进程的是子进程的PID。

内核线程

现代操作系统将一些重要的任务交给内核线程,内核线程不受不必要的用户态上下文的拖累,这些任务包括刷新磁盘高速缓存,交换出不用的页框,维护网络连接等等。在Linux中,内核线程在以下几个方面不用于普通进程:

内核线程之运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。

内核线程只使用大于PAGE_OFFSET的线性地址空间,普通进程可以用4GB的线性地址空间。

创建一个内核线程

kernel_thread()函数创建一个新的内核线程,它接受的参数由:所要指向的内核函数的地址(fn)、要传递给函数的参数(arg)、一组clone标志(flags)。该函数本质以下面方式调用do_fork():

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

CLONE_VM标志避免复制调用进程的页表,CLONE_UNTRACED标志保证不会有任何进程跟踪。

传递给do_fork()的参数pregs表示内核栈的地址,copy_thread()函数将从这里找到新线程初始化CPU寄存器的值。

进程0

所有进程的祖先叫做进程0,idel进程或因为历史的渊源叫做swapper进程,它是在Linux的初始化阶段从无到有创建的一个内核线程,这个祖先进程使用下列静态分配的数据结构:

存放在 init_task 变量中的进程描述符,由 INIT_TASK 宏完成对它的初始化

存放在 init_thread_union 变量中的 thread_info 描述符和内核堆栈,由 INIT_THREAD_INFO 宏完成对它们的初始化

start_kernel()函数初始化内核需要的所有数据结构,激活中断,创建另一个叫进程1 的内核线程(一般叫做init进程):

kernel_thread(init, NULL, CLONE_FS|CLONE_SIGHAND);

新创建的内核线程的PID为1,并于进程0共享每进程所有的内核数据结构。此外,当调度程序选择到它时,init 进程开始执行init()函数。

在多处理器系统中,每个CPU都有一个进程0。打开电源,计算机的BIOS就启动某一个CPU,同时禁用其他CPU。运行在CPU 0上的swapper进程初始化内核数据结构,然后激活其他的CPU,并通过copy_process()函数创建另外的swapper进程,把0传递给新创建的swapper进程作为它们的新PID。

进程1(init进程)

由进程0创建的内核线程执行init()函数,init()依此完成内核初始化。init()调用execve()系统调用装入可执行程序init。结果,init内核线程变成一个普通进程,且拥有自己的每进程内核数据结构。在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。

其他内核线程

Linux使用很多其他内核线程,其中一些在初始化阶段创建,一直运行到系统关闭,而其他一些在内核必须执行一个任务时“按需”创建,这种任务在内核的执行上下文中得到很好的执行。

进程撤销

进程终止

在Linux2.6中有两个终止用户态应用的系统调用:

exit_group()系统调用,它终止整个线程组,即整个基于多线程的应用。do_group_exit()是实现这个系统调用的主要内核函数。这是C库函数应该调用的系统调用。

exit()系统调用,它终止某一个线程,而不管该线程所属线程组中的所有其他线程。do_exit()是实现这个系统调用的主要内核函数,这是被诸如 pthread_exit()的Linux线程库的函数所调用的系统调用。

关键词: