深入理解Linux内核(学习笔记)_第三章进程

进程是任何多道程序设计的操作系统中的基本概念。通常把进程定义为程序执行的一个实例。在Linux源代码中,常把进程称为任务(task)或线程(thread)。

一.进程、轻量级进程和线程

       从内核观点看,进程的目的就是担当分配系统资源(cpu时间、内存等)的实体。多线程应用程序仅仅是一个普通进程,多线程应用程序多个执行流的创建、处理、调度整个都是在用户态进行的。Linux使用轻量级进程对多线程应用程序提供更好的支持。实现多线程应用程序的一个简单方式就是把轻量级进程与每个线程关联起来,这样线程之间就可以通过简单地共享同一内存地址空间、同一打开文件集等来访问相同的应用程序数据结构集,同时每个线程都可以由内核独立调度,以便一个睡眠的同时另一个仍然是可运行的。

二.进程描述符

       为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。例如,内核必须知道进程的优先级,它是正在CPU上运行还是因某些事件而被阻塞,给它分配了什么样的地址空间,允许它访问那个文件等等。这正是进程描述符的作用——进程描述符都是task_struct类型结构,它的字段包含了与一个进程相关的所有信息。因为进程描述符中存放了那么多信息,所以它是相当复杂,它不仅包含了很多进程属性的字段,而且一些字段还包括了指向其它数据结构的指针,依次类推。

  • 进程状态:可运行状态、可中断的等待状态、不可中断的等待状态、暂停状态、跟踪状态、僵死状态、僵死撤销状态。
  • 标识一个进程:进程与进程描述符之间有非常严格的一一对应关系,这使得用32位进程描述符地址标识进程成为一种方便的方式。进程描述符指针指向这些地址,内核对进程的大部分引用是通过进程描述符指针进行的。linux把不同的PID与系统中每个进程或轻量级进程相关联。这种方式能提供最大的灵活性,因为系统中每个执行上下文都可以被唯一地识别。一个线程组中的所有线程使用和该线程组的领头线程相同的PID,也就是该组中第一个轻量级进程的PID,它被存入进程描述符的tgid字段。1)进程描述符处理:进程是动态实体,其生命周期范围从几毫秒到几个月。因此必须能够同时处理很多进程,并把进程描述符存放在动态内存中,而不是放在永久分配给内核的内存区。对每个进程来说,Linux都把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内:一个是与进程描述符相关的小数据结构thread_info,叫做线程描述符;另一个是内核态的进程堆栈。2)标识当前进程:从效率的观点来看,刚才所讲的thread_info结构与内核态堆栈之间的紧密结合提供的主要好处是内核很容易从esp寄存器的值获得当前在CPU上正运行进程的thread_info结构的地址。事实上,如果thread_union结构长度是8K(2^13字节),则内核屏蔽掉esp的低13位有效为就可以获得thread_info结构的基地址;而如果thread_union结构长度是4K,内核需要屏蔽掉esp的低12位有效位。这项工作由current_thread_info()函数来完成。3)双向链表:对每个链表,必须实现一组原语操作:初始化链表,插入和删除一个元素,扫描链表等等,每个链表都要重复相同的原语操作而造成存储空间浪费。因此linux内核定义了list_head数据结构,字段next和prev分别表示通向链表向前和向后的指针元素。4)进程链表:进程链表就是把所有进程的描述符链接起来,每个task_struct结构都包含一个list_head类型的tasks字段,这个类型的prev和next字段分别指向前面和后面的task_struct元素。进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符。5)Task_running状态的进程链表:当内核寻找一个新进程在CPU上运行时,必须只考虑可运行进程。提高调度程序运行速度的诀窍是建立多个可运行的进程链表,每种进程优先权对应一个不同的链表,每个task_struct描述符包含一个list_head类型的字段run_list。内核必须为系统中每个运行队列保存大量的数据,不过运行队列的主要数据结构还是组成运行队列的进程描述链表,所有这些链表都有一个单独的prio_array_t数据结构来实现。
  • 进程间的关系:程序创建的进程具有父/子关系。如果一个进程创建多个子进程时,则子进程之间具有兄弟关系。pidhash表及链表:散列函数并不总能确保PID与表的索引一一对应,两个不同的PID散列(hash)到相同的表索引称为冲突。Linux利用链表来处理冲突的PID:每个表项是由冲突的进程描述符组成的双向链表。
  • 如何组织进程:运行队列链表把处于Task_running状态的所有进程组织在一起。1)等待队列:等待队列在内核中有很多用途,尤其用作中断处理、进程同步及定时。等待队列由双向链表实现,其元素包括指向进程描述符的指针。2)等待队列操作:函数init_waitqueue_head()可以用来初始化动态分配的等待队列的头变量。sleep_on()对当前进程进行操作等等。
  • 进程资源限制:每个进程都有一组相关的资源限制,限制指定进程能使用的系统资源数量。这些限制避免用户过分使用系统资源(cpu、磁盘空间等)。

三.进程切换

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

  • 硬件上下文:进程恢复前执行必须装入寄存器的一组数据称为硬件上下文。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。
  • 任务状态段:任务状态段(TSS)来存放硬件上下文。
  • 执行进程切换:进程切换可能只发生在精心定义的点:schedule()函数,从本质上说,每个进程切换由两步组成:1)切换页全局目录以安装一个新的地址空间;2)切换内核堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。switch_to宏,__switch_to()函数。
  • 保存和加载FPU、MMX及XMM寄存器:保存FPU寄存器save_init_fpu()、装载FPU寄存器restore_fpu,在内核态使用FPU、MMX和SSE/SSE2单元。

四.创建进程

      传统的Unix操作系统以统一的方式对待所有的进程:子进程复制父进程所拥有的资源,这种方法使进程的创建非常慢且效率低,因为子进程需要拷贝父进程的整个地址空间。现代Unix内核通过引入三种不同的机制解决了这个问题:1)写是复制技术允许父子进程读相同的物理页;2)轻量级进程允许父子进程共享每进程在内核的很多数据结构,如页表、打开文件表及信号处理;3)vfork()系统调用创建的进程能共享其父进程的内存地址空间。

  • clone()、fork()及vfork()系统调用:clone()函数、do_fork()函数、copy_process()函数;
  • 内核线程:在linux中,内核线程在以下几个方面不同于普通进程:1)内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态;2)因为内核线程只运行在内核态,它们只使用大于PAGE_OFFSET的线性地址空间,另一方面,不管在用户态还是在内核态,普通进程可以用4GB的线性地址空间。创建一个内核线程:kernel_thread()函数创建一个新的内核线程,它接受的参数有:所要执行的内核函数的地址(fn)、要传递给函数的参数(arg)、一组clone标志(flags)。进程0:所有进程的祖先叫做进程0。进程1:由进程0创建的内核线程执行init()函数,init()依次完成内核初始化。其他内核线程:按上下文需求创建,“按需”创建。

五.撤销进程

  • 进程终止:exit_group系统调用:它终止整个线程组,即整个基于多线程的应用;exit()系统调用,它终止某一个线程,而不管该线程所属线程组中的所有其他进程。
  • do_group_exit()函数:该函数杀死属于current线程组的所有进程。
  • do_exit()函数:所有进程的终止都是由do_exit()函数来处理的,这个函数从内核数据结构中删除对终止进程的大部分引用。
  • 进程删除:release_task()函数从僵死进程的描述符中分离出最后的数据结构;对僵死进程的处理有两种可能的方式:如果父进程不需要接受来自子进程的信号,就调用do_exit();如果已经给父进程发送了一个信号,就调用wait4()或waitpid()系统调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值