Linux进程管理-linuxnote01

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/abm1993/article/details/82384290

进程管理是所有操作系统的心脏所在,Linux也不例外。(本篇及后续所有的Linux学习都基于”LINUX内核修炼之道任桥伟著“和”Linux内核设计与实现“这两本书。站在巨人的肩上,我们得以往的更远。)

进程和线程

进程就是处于执行期间的程序(目标代码存在某种存储介质上),进程是动态的,程序是静态的。但进程不仅仅局限于一段可执行的代码,通常进程还要包含其他资源,比如打开的资源,挂起的信号,内核内部数据,处理器状态等等一些资源。从内核的角度来说,进程是操作系统分配内存,CPU时间片等资源的基本单元,为正在运行的程序提供运行环境。

线程,在Linux中,线程和进程其实没有本质区别,在Linux中并没有特别针对线程进行额外描述的数据结构。线程在内核中体现只是一个普通的进程,他拥有自己的进程描述符,程序计数器,进程栈等,只不过该进程与其他的一些进程共享某些资源,比如地址空间,打开的文件等,这样的进程也称为轻量级进程(Light Weight Porcess LWP)。也可以这么说内核调度的其实是线程,而不是进程。

疑惑点整理:进程的地址空间,内核栈,进程栈。

进程描述符

内核使用进程描述符(task_struct)来描述一个与进程相关的所有信息,内核使用是一个叫做任务队列(task list)的双向循环链表结构来存放进程列表。进程描述符中包含数据能够完整地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态等等更多的信息。Linux通过slab分配器分配task_struct结构,由于进程是动态存在不断变化的实体,所以分配的task_struct必须常驻内存之中,另外进程可以通过系统调用或者异常状态由用户状态切换到内核状态之后,也需要栈空间用开进行函数调用。因此内核给每个进程都分配了一个固定大小的内核栈,用于保存进程在内核状态中的函数调用链以及进程描述符。slab分配器分配的task_struct就放于内核栈的尾部。由于内核栈固定大小,为了避免进程描述符越来越大,内核引入了thread_info结构,取代了task_struct在内核栈的位置,现在slab分配器会在内核栈的尾部创建一个新的thread_info,而thread_info内部会有一个task狱指向该进程实际task_struct的指针。

 疑惑点整理:task_struct放在哪里,slab分配器,内核栈放在哪里

进程状态

TASK_RUNNING 可运行状态 进程可执行的状态,它或者正在执行,或者在运行队列中等待执行
TASK_INTERRUPTIBLE 可中断等待状态 进程正在睡眠也就是说它被阻塞,等中某种条件,一旦条件达成便设置为可运行
TASK_UNINTERRUPTIBLE 不可中断等待状态 和中断状态相同,但是不接受任何信号
__TASK_STOPPED 暂停状态 进程停止运行,收到SIGSTOP就会进入此状态
__TASK_TRACED 跟踪状态 被其他进程跟踪的状态,一般调试的时候出现此状态
EXIT_DEAD 僵死撤销状态 该状态是exit_state字段的值,表示进程的最终状态,父进程已经调用wait4或waitpid系统调用
EXIT_ZOMBIE 僵死状态 该状态是exit_state字段的值,表示进程执行被终止,但是父进程未调用wait4或waitpid系统调用来返回有关终止进程的信息,在此操作之前,内核不能丢弃在子进程描述符中的数据
TASK_DEAD 死亡状态 区别exit_state,一个进程退出时state被设置为TASK_DEAD

 一般程序都是在用户空间执行,当进程执行了系统调用或者触发了某一个异常,它就陷入了内核空间。此时我们称内核代表进程执行,并处于进程上下文中。

进程创建

相比于其操作系统使用spawn机制产生进程,Linux内核采用了与众不同的实现方式,fork+exec。fork通过拷贝当前进程创建一个子进程,exec负责读取可执行文件将其载入地址空间并执行。子进程与父进程的区别在于PID,PPID,和某些资源和统计量。

Linux的进程依据特点可分为三种:idle进程(进程号为0,一般每个cpu都会有一个idle进程,idle进程运行的时间便是对应cpu空闲的时间),内核进程(一般命名以d结尾),用户进程。

除了fork之外,Linux中还有vfork和clone两种方式用于创建进程,系统调用fork,vfork,clone在内核的服务例程分别为sys_fork(),sys_vfork(),sys_clone,最终它们都会调用do_fork。使用fork创建进程时,子进程完全复制父进程的资源,并独立于父进程,父子进程具有很好的并发性,但他们之间的通信需要使用专门的IPC进程通信机制,由于创建时完全复制父进程的资源,内核采用了写时复制的技术(copy-on-write)对fork进行了优化,此时父子进程以只读的方式共享父进程的资源,只有在子进程试图修改进程地址空间上的某一页,才进行该页的复制。使用vfork创建进程时,不会复制父进程的页表,父子进程完全共享进程的地址空间,可以这么说子进程完全运行于父进程的地址空间里,子进程对其中修改的任何数据都为父进程所见。使用clone创建进程时,不同于fork完全复制父进程的资源,clone可以通过不同的参数组合选择性的复制父进程的资源,甚至可以让新创建的进程是兄弟关系。

疑惑点整理:copy-on-write技术如何实现

进程终结

无论如何,进程终归是要死亡的,当一个进程退出时,内核必须释放它所占用的资源,并把这个不幸告知它的父进程。进程退出一般是由于显示或隐式的调用exit(),或者接受了自己既不能处理又不能忽略的信号,然而,不管处于什么原因,进程的退出最终都会调用do_exit()来处理。do_exit()的目的是删除当前进程的所有引用(对于非共享资源),一般涉及几个关键部分:获取当前被终止的进程描述符并设置进程的标志为PF_EXITING,表明进程正在退出,删除进程生命周期内得到的各种资源(如果这些资源没有其他进程引用,就将它们彻底删除),调用exit_notify更新父子进程的亲属关系(此举针对孤儿进程,当父进程在子进程之前推出,则子进程便成为孤儿进程,如果不为孤儿进程重新寻父,则子进程永远处于僵死状态),并发出通知比如告诉父进程它正在退出同时设置进程状态(exit_state)为EXIT_ZOMBIE,调用schedule()切换到其他进程,因为之前进程被设置为EXIT_ZOMBIE状态,因此进程不会被调度,此时它所占用的内存就是内核栈,thread_info,task_struct,保留这些内存的目的就是向它的父进程提供信息,父进程检索到信息后,或者通知内核那是无关的信息后,这些内存最终被释放,归还系统。

写在最后

本篇文章,我们学习了Linux进程,学习了如何存放和描述进程(task_struct和thread_info),如何创建进程(fork,vfork,clone,实际最终是clone,它们的区别在于配合使用了不同的参数),如何将新的映像装入到地址空间(通过exec系统调用),以及最终进程如何终结(exit调用)。学习过程中,也不免有许多疑惑的地方,进程的地址空间是什么,它存放在哪里?进程的创建运行过程中内核栈何时创建,存在哪里?进程栈又是什么?task_struct使用slab分配器分配,放哪儿?slab是什么?进程创建时使用fork,copy-on-write技术如何实现?等等诸多的疑问。我们暂且先放放这些疑问,在后续的学习中,排忧解惑。

猜你喜欢

转载自blog.csdn.net/abm1993/article/details/82384290