进程类似于人类:他们被产生,有或多或少有效的生命,可以产生一个或多个子进程,最终都要死亡。一个微小的差异是进程之间没有性别差异——每个进程只有一个父亲。
我们提供的服务有:网站制作、网站建设、微信公众号开发、网站优化、网站认证、营口ssl等。为上千多家企事业单位解决了网站和推广的问题。提供周到的售前咨询和贴心的售后服务,是有科学管理、有技术的营口网站制作公司从内核的观点来看:进程的目的就是担当分配系统资源的实体(CPU时间、内存等资源)。
实现多线程应用的一个简单的方式是把轻量级进程与每个线程关联起来。这样线程之间就可以通过简单的共享同一内存地址空间、同一打开文件集等来访问相同的应用数据结构集;同时每个线程都可以由内核独立调度,以便一个睡眠的同时另一个仍然是可运行的。(共享内存地址、内核独立调度)。
进程描述符
进程描述符都是task_struct类型结构,它的字段包含了一个进程相关的所有信息。
进程状态
进程描述符的state字段描述了进程当前所处的状态。它由一组标志组成,其中每个标志描述一种可能的进程状态。在当前linux版本中,这些状态是互斥的,只能设置一种状态;其余的标志将被清除。
1、可运行TASK_RUNNING 进程在CPU上要么正在执行,要么准备执行
2、可中断TASK_INTERUPTIBLE 进程被挂起,直到达到某个条件。产生一个硬件中断,释放进程正等待的系统资源,或传递一个信号都是可以唤醒进程的条件。
3、不可中断 TASK_UNINTERUPTIBLE 与可中断状态类似,但是把信号传递到不可中断状态进程时不能改变其状态。例如,当进程打开一个设备文件,其相应的设备驱动程序开始探测相应的硬件设备时会用到这种状态。探测完成之前,设备驱动程序不能被中断,否则,硬件设备会处于不可预知的状态。
4、暂停状态TASK_STOPPED 进程的执行被暂停。当进程受到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号后进入暂停状态
5、TASK_TRACED跟踪状态 进程的执行已经有debugger程序暂停。
以下两个进程状态既可以存放到state字段又可以存放到exit_state字段
6、僵死状态(EXIT_ZOMBLE) 进程的执行被终止,但是父进程还没有发布wait4()或waitpid()系统调用来返回有关死亡进程的信息。
7、僵死撤销状态(EXIT_DEAD) 最终状态:由于父进程刚发出wait4()系统调用,因而进程由系统删除。为了防止其他执行线程在同一个进程上也执行wait()类系统调用而产生竞争条件,而把僵死状态改为撤销状态。
标识一个进程
由于循环使用PID编号,内核必须通过管理一个pidmap_array的位图来标识当前已经分配的pid号和闲置的pid号。因为一个页框包含4*1024*8个位,所以32位体系结构中pidmap_array位图存放在一个单独的页中。一个多线程应用中的所有线程都必须有相同的pid。Linux引入线程组:一个线程组中的所有线程使用和该线程组领头线程相同的pid。它被存入进程描述符的tgid字段中。getpid()系统调用返回当前进程的tgid值而不是pid值。
进程描述符处理
对每个进程来说,Linux都把两个不同的数据结构紧凑的放在一个单独为进程分配的存储区域中:一个是与进程描述符相关的小数据结构thread_Info,一个是内核态的进程堆栈。这块存储区域的大小一般为2页。考虑到效率的因素,内核让这个8k空间占连续两个页框并让第一个页框的起始地址是2^13的倍数。
因为内核控制路径使用很少的栈,因此只需要几千个字节的内核态堆栈。对已,对栈和thread_info来说,8k足够了。
thread_info存放在这个8k内存区域的开始,而栈从内存区域的末端向下增长。
esp寄存器时CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于末端,并朝这个内存区域开始的方向增长。从用户态刚切换到内核态后,进程的内核栈总是空的,因此,esp寄存器这是指向这个栈的顶端。一旦数据写入栈,esp的值就递减。因为thread_Info的结构式52个字节长,因此内核栈能扩展到8140个字节。
标识当前进程
如果thread_union结构长度是8k,则内核屏蔽掉esp的低13位就可以获得thread_info的基地址;如果thread_union结构的长度是4k,内核需要屏蔽掉esp的低12位。这项工作由current_thread_info()函数来完成,它产生如下汇编指令:
movl $-8192, %ecx //将19个1+00000000000000(低13位为0)的值保存到ecx寄存器
andl %esp, %ecx //取出ecx中的值与esp中当前栈顶单元的地址进行位与运算,即屏蔽esp的后13位。
movl %ecx ,p //将计算的结构赋值给p
因为task字段在thread_info结构中的偏移量为0,所以执行完以上指令后,p就包含在cpu上运行进程的进程描述符指针。
双向链表
linux定义了list_head数据结构,字段next和prev分别表示通用双向链表向前和向后的指针元素。list_head字段的指针中存放的是另一个list_head字段的地址,而不是含有list_head结构的整个数据结构的地址。
双向链表处理函数和宏
list_add(n,p)
list_add_tail(n,p)把n指向的元素插在p指向的元素之前。
list_del(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_for_each类似,但是返回了包含list_head结构的数据结构的地址,而不是list_head结构本身的地址
进程链表
进程链表的头是init_task描述符,init_task的tasks.prev字段指向链表中最后插入的额进程描述符的tasks字段。
SET_LINKS和REMOVE_LINKS宏分别用于从进程链表中插入和删除一个进程描述符。
宏for_each_process扫描整个进程链表定义如下:
#define for_each_process(p)
for(p=&init_task;(p=list_entry((p)->tasks.next, struct task_struct,tasks))!=&init_task; )
TASK_RUNNING状态的进程链表
提高调度程序运行速度的诀窍是建立多个可运行进程链表,每种进程优先级权限对应一个不同的链表。每个task_struct描述符包含一个list_head类型字段run_list。如果进程的优先权等于k,run_list字段把进程链接到优先权为k的可运行进程链表中。所有这些可运行进程链表由一个单独的prio_array_t数据结构来实现。
prio_array_t数据结构字段:
int nr_active 链表中进程描述符的数量
unsigned long[5] bitmp 优先权位图;当且仅当某个优先权的进程链表不为空时设置相应的标志位
struct list_head[140] queue 140个优先权队列的头结点。
进程间的关系
进程描述符中表示进程进程亲属字段的描述
real_parent 指向创建了p的进程描述符,如果p的父进程不存在,就指向1的进程描述符
parent 指向p的当前父进程。它的值常常与real_parent一致,偶尔不同:当一个进程发出监控p的ptrace()系统请求时
children 链表的头部,链表中的所有元素都是p创建的子进程
sibling 指向兄弟进程链表中的下一元素或前一个元素的指针,这些兄弟进程的父进程都是p
建立非亲属关系的进程描述符字段
group_leader p所在进程组的领头进程的描述符指针
signal->pgrp p所在进程组领头进程的pid
tgid p所在线程组的领头进程pid
signal->session p所在登录会话领头进程的pid
ptrace_children 链表的头,该链表包含所有被debugger程序跟踪的p的子进程
ptrace_list 指向所跟踪进程其实际父进程链表的前一个和下一个元素
pidhash表及链表
内核必须能从进程的pid导出对应的进程描述符指针。顺序扫描进程链表并检查进程描述符的pid字段是可行但是相当的低效。为了加速查找,引入了4个散列表。
4个散列表和进程描述符中的相关字段
PIDTYPE_PID pid 进程的PID
PIDTYPE_TGID tgid 线程组领头进程的pid
PIDTPE_PGID pgrp 进程组领头进程的pid
PIDTYPE_SID session 回话领头进程的pid
内核初始化期间动态的为这4个散列表分配空间,并把他们的地址存入pid_hash数组中。一个散列表的长度依赖于可用的ram容量。
用pid_hashfn宏把PID数值转化为散列表的表索引。
Linux利用链表来处理散列表中冲突的pid:每一个表项是有冲突的进程描述符组成的双向链表。
具有链表的散列法比从pid到表索引的线性转换更优越,因为在任何给定的实例中,系统中的进程数总是远远小于32768。如果在任何给定的实例中大部分表项都不使用的话,那么把表定义为32768项会是一种存储浪费。
由于需要跟踪进程间的关系,pid散列表中使用的数据结构非常复杂。如果根据线程组号查找散列表,只能返回一个进程描述符,就是线程组领头进程的描述符。为了能快速返回组中其他所有的进程,内核就必须为每个线程组保留一个链表。PID散列表的数据结构解决了这个问题。因为他们可以为包含在一个散列表中的任何pid号定义进程链表。针对四中散列表,定义了四个pid结构的数组,它在进程描述符的pids字段中。
pid结构的字段
int nr pid的数值
struct h_list_node pid_chain 链接散列表的下一个和前一个元素
struct list_head pid_list 链接相同pid值的进程链表的标头
如何组织进程
运行队列链表把处于TASK_RUNNING状态的所有进程组织在一起;没有为处于TASK_STOPPED、EXIT_ZOMBLE或EXIT_DEAD状态的进程建立专门的链表。由于对处于这些状态的进程访问比较简单,或者通过pid或者通过特定父进程的子进程链表,所有不必对这三种状态进程分组。
根据不同特殊事件把处于TASK_INTERUPTIBLE和TASK_UNinteruptible状态的进程细分为许多类,将这些进程链接到等待队列。
等待队列
等待队列由双向链表实现,其元素包括指向进程描述符的指针。每个等待队列有一个等待队列头wait_queue_head_t;因为等待队列是有中断处理程序和主要内核函数修改的,因此必须对其双向链表进行保护,同步是通过等待队列头中的lock自旋锁达到的。等待队列链表中的元素类型为wait_queue_t.
有两种睡眠进程:互斥进程(等待队列元素的flags字段为1)由内核有选择的唤醒,而非互斥进程(flags=0)总是由内核在事件发生时唤醒。等待访问临界资源的进程是互斥进程的例子,等待相关事件的进程是非互斥的。
等待队列的操作
定义一个等待队列:DECLARE_WAIT_QUEUE_HEAD(name)定义一个等待队列的头init_waitqueue_head()可以用来初始化动态分配的等待队列的头变量。
初始化wait_queue_t结构变量:init_waitqueue_entry(q,p);DEFINE_WAIT宏
插入等待队列:add_wait_queue()非互斥 第一个位置;add_wait_queue_exclusive()互斥最后一个位置
移除:remove_wait_queue()
判断队列为空:waitqueue_active()
要等待特定条件的进程可以条用如下函数:
sleep_on();
interruptible_sleep_on();
sleep_on_timeout();interruptible_sleep_on_timeout();
prepare_to_wait();finsh_wait();prepare_to_wait_exclusive();
wait_event;wait_event_interruptible;
唤醒:各种唤醒函数,不举例。
唤醒:非互斥进程p将有default_wake_function()唤醒
进程资源限制
每个进程都由一组相关资源的限制,限制了进程能使用的系统资源数量。对当前进程的资源限制存放在current->signal->rlim字段,即进程信号描述符的一个字段。该字段类型为rlimt结构的数组,每个资源限制对应一个元素:
struct rlimit{
unsigned long rlim_cur;
unsigned long rlim_max;
}
rlim_cur字段是资源的当前资源限制。rlim_max字段是组员限制所允许的大值。
进程切换
为了控制进程的执行,内核必须有能力挂起当前cpu上运行的进程,并恢复以前挂起的某个进程执行。这种行为被称为进程切换、任务切换或上下文切换。
硬件上下文
进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行所需要的所有信息。linux中,进程硬件上下文的一部分存放在TSS段,而剩余部分存放在内核态的堆栈中。
进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器内容已经保存在内核态堆栈上,这也包括ss和esp这对寄存器的内容(存储用户态堆栈指针的地址)。
任务状态段
尽管linux不使用硬件上下文切换,但是强制它为系统中不同的cpu创建一个TSS。因为:
1、cpu从用户态切换到内核态时,它就从TSS中获取内核态堆栈的地址。
2、当用户态进程视图通过in或out指令访问一个I/O端口时,cpu需要访问存放在TSS中的I/O许可权位图,以检查该进程是否有访问端口的权力。
tss_struct结构描述TSS的格式。每次进程切换时,内核都更新TSS的某些字段以便相应的cpu控制单元可以安全的检索到它需要的信息。因此,TSS反应了CPU上当前进程的特权级别,但不必为没有运行的进程保留TSS。
每个TSS有它自己8字节的任务状态段描述符。这个描述符包括指向TSS起始地址的32位base字段,20位limit字段。s标志位被清0,以表示相应的TSS是系统段。type字段值为11或9以表示这个段实际上是TSS。每个cpu的tr寄存器包含相应TSS的TSSD选择符,也包含了两个隐藏的非编程字段base和limit。这样处理器可以直接对TSS寻址而不用从GDT中检索TSS的地址。
thread字段
每个进程描述符包含一个类型为thread_struct的thread字段,只要进程被切换出去,内核就把硬件上下文保存在这个结构中。
执行进程切换
从本质上说,进程切换由两步组成:
1、切换页全局目录以安装一个新的地址空间;
2、切换内核堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含cpu寄存器。
switch_to宏
__switch_to()函数
创建进程
1、copy on write机制允许父子进程读相同的物理页。只要两者中有一个视图写一个物理页,内核就把这个页的内容拷贝到一个新的物理页,并把这个新的物理页分配给正在写的进程。
2、轻量级进程允许父子进程共享每进程在内核的很多数据结构,如页表、打开文件表及信号处理。
3、vfork()系统调用创建的进程能共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出或执行一个新的程序为止。
clone()、fork()和vfork()系统调用
do_fork()函数负责处理clone()、fork()、vfork()系统调用
copy_process()创建进程描述符以及子进程执行所需要的所有其他数据结构
内核线程
与普通进程的区别:1、内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。2、内核线程只能使用大于PAGE_OFFSET的线性地址空间。
创建一个内核线程
kernel_thread()函数创建一个新的内核线程
进程0和进程1以及其他内核线程
撤销进程
进程终止:exit_group()终止线程组;exit()终止一个线程
do_group_exit();
do_exit();
进程删除 release_task()函数从僵死进程的描述符中分离出最后的数据结构;对僵死进程的处理方式:如果父进程不需要接受来自自进程的信号,就do_exit();如果已经给父进程信号,就调用wait4()或waitpid(),将回收进程描述符所占用的内存空间。在前一种情况下,内存的回收将有进程调度程序来完成。