Linux内核学习3:进程管理1

2019-07-12 16:47发布

笔者在进行嵌入式Linux学习的过程中,在Linux进程控制程序设计中学习了两个部分:一是进程控制理论基础,二是进程控制编程。 在Linux进程控制程序设计中主要说的是多进程程序设计和进程间通信的问题,到后期说到进程管理的问题时,始终没有把二者联系在一起。 顾名思义,在进行进程控制程序设计的时,讲的最多的就是如何创建、删除进程,如何完成进程间的通信。而进程管理讨论Linux内核如何管理每个进程:它们在内核中如何被列举,如何创建,最终如何消亡。 概念:进程与线程 进程:程序本身不是进程,进程是处于执行期的程序以及相关的资源总称 进程描述符和任务结构:
内核把进程的列表存放在任务队列的双向循环列表中。
链表中每一项都是类型为task_struct、称之为进程描述符的结构,结构定义在中,每个进程描述符包含一个具体进程的所有信息。进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间,挂起的信号。进程状态,还有其他更多的信息。 进程描述符的存放:内核通过一个唯一的进程标识值或者PID来标识每个进程。(标识进程的唯一数字) 内核把每个进程PID存放在它们各自的进程描述符中。 进程状态:进程描述符中的state域描述了进程的当前状态。 系统中每个进程都必然处于五种状态中的一种:TASK_RUNNING(运行)、TASK_INITERRUPTIBLE(可中断)、TASK_UNINITERRUPTIBLE(不可中断)、_TASK_TRACED(被其他进程跟踪的进程)、_TASK_STOPPED(停止)。 其实进程可以理解为三态过程:执行、阻塞、就绪。 进程上下文:可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。 一般程序是在用户空间执行。当一个程序执行了系统调用或者触发了某个异常,就陷入了内核空间。系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能进入内核空间执行——对内核的所有访问都必须通过这些接口。(在文件编程中经常提到文件编程的几种方式:里面就有系统调用和库函数实现。这里面的系统调用大概就是这个意思 进程家族树:UNIX系统进程之间存在一个明显的继承关系,在Linux系统中也是如此。所有进程都是PID为init进程的后代。 内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程。 关于线程与进程之间的区别: 在回答这个问题时,先来说下线程,线程机制提供了再同一程序内共享内存地址空间运行的一组线程。其实意思就是在一个进程中,线程是共享内存地址空间的。也就是说一个进程可以有多个线程。通俗的而言,线程共享了进程的资源,是一个轻量级的进程。 在具体回答线程与进程区别时,进程是资源分配的最小单位,线程是调度的最小单位。除此之外,进程有独立的用户空间。在用户空间层面上而言,线程是共享了进程的用户空间。而内核线程不具有用户空间。 关于内核线程而言,内核经常需要在后台执行一些操作。这种任务通过内核线程完成——独立运行在内核空间的标准进程。 内核线程与普通进程最大的区别在于:内核线程没有独立的地址空间(实际上指向地址空间的mm指针为空)。它们只在内核空间运行,从不切换到用户空间去。 进程控制编程 在进程控制编程中主要有关于进程id的获取,创建进程,进程的终结 1:进程id的获取 pid_t getpid(void); pid_t getppid(void); pid_t实际上是隐式类型,就是int型 获取例程: #include #include #include int main() { printf("PID=%d ",getpid()); printf("PPId=%d ",getppid()); return 0; }
2:进程创建 许多其他操作系统都提供了产生进程的方法,首先是在新的地址空间里创建进程,读入可执行文件,最后执行 但是UNIX系统采用了与众不同的实现方式,它把上述的步骤分解成两个单独的函数去执行:fork()和exec函数族。 步骤:首先fork()函数拷贝当前的进程创建一个新的进程。此时子进程拥有了父进程的所有信息,继承了所有的数据,区别只是进程号的差异       再者,exec()函数族负责读取可执行文件并将其载入地址空间开始运行。 1)fork 对于fork的理解一定要注意:
  • fork创建时,当父进程从上而下执行的时候,调用fork创建一个子进程,在父进程中fork返回子进程的PID,在子进程中返回一个0;
  • fork创建的子进程与父进程共享代码段。数据段只是拷贝,并不是共享。子进程的数据空间、堆栈空间都会从父进程得到一个拷贝,而不是共享。比如说子进程中的数据发生改变时,并不会影响到父进程。
例程如下:#include #include #include int main() { printf("PID=%d ",getpid()); printf("PPId=%d ",getppid()); return 0; } 上述的结果是父进程中count=1;子进程中count=1,因为只是数据的复制,并不是共享,所以子进程的数据变化不影响父进程。 2)vfork 其实vfork与fork之间只要了解它们之间的区别就可以了:
  • fork:子进程拷贝父进程的数据段
  • vfork:子进程共享父进程的数据段
  • fork:父、子进程的执行次序不确定
  • vfork:子进程先进行,父进程后执行。
因此将上述的代码修改成vfork以后:执行的结果是第一次子进程count=1;第二次父进程:count=2; 3)exec函数zuexec用被执行的程序替换调用它的程序 区别:fork创建一个新的进程,产生一个新的PID exec启动一个新的程序,替换原有的程序,因此进程的PID不会改变。这也解释了exec负责负责读取可执行文件并将其载入地址空间开始运行。 比如说:正在执行一个程序,当读到exec时,假设里面有个hello.c程序,此时就用hello.c程序替换掉原来的代码。 在exec中有execl、execlp、execv、system系统调用。
  • execl:
#include void main() { execl("/bin/ls","ls","-al","/etc/passwd",(char*)0);函数原型:int execl(const char *path,const char *arg1.......)path:被执行程序名(含完整路径) arg:执行的命令行参数,最后以(char*)0结束。
  • execlp
函数原型:int execlp(const char*path,const char*arg1,.....); path:被执行程序名(不包含文件,将从path环境变量中查找程序) arg:被执行程序所需要的命令行参数,含有程序名
  • execv
函数原型:int execv(const char*path,char*const arg[]); path:被执行程序名(含有完整路径) argv[]:被执行程序所需要的命令行参数数组 例程: int main() { char *argv[]={"ls","-al","/etc/passwd",(char*)0}; execv("/bin/ls",argv) }
  • system系统调用
函数原型:#include int system(const char*string);功能:调用fork产生子进程,由子进程调用”/bin/sh.-c string “来执行string所代表的命令#include void main() { system("ls -al /etc/psaswd"); }区别在于:调用了fork产生一个子进程,之后执行exec函数族的功能。 3)进程的终结 进程终归是要终结的,当终结时,内核必须释放它所占有的资源,并把这一信息告知其父进程。 一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,即可显式的调用这个系统调用,也可能隐式的从某个程序主函数返回。不管怎么终结,该任务大部分都要靠do_exit() 4)进程等待 在调用do_exit()以后,尽管线程已经僵死不能再运行,但是系统还是保留有了它的进程描述符。这么做可以让系统有办法在子进程终结以后仍然获得它的信息。 因此进程终结时所需的清理工作和进程描述符的删除被分开。 而进程等待的作用就是:阻塞该进程,直到其某个子进程退出。