进程是Linux操作系统环境的基础,它控制着系统上几乎所有的活动。
目录
1、什么是进程
UNIX标准把进程定义为:“一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源”。进程是UNIX操作系统最基本的抽象之一,可以把一个进程看作是一个正在运行的程序。Linux是多任务操作系统,可以同时运行多个程序,每个运行着的程序就构成一个进程。作为多用户系统,Linux允许许多用户同时访问系统。每个用户可以同时运行许多个程序,甚至同时运行同一程序的的许多实例。系统本身也运行着一些管理系统资源和控制用户访问的进程。
但进程并不仅局限于一段可执行代码(UNIX称其为代码段)。还由打开的文件(文件描述符)、挂起的信号、内核内部数据、处理器状态、地址空间及一个或多个执行的线程,还要用来存放全局变量的数据段。一般来说,Linux系统会在进程之间共享程序代码和系统函数库,所以,在任何时刻内存中都只有代码的一份副本。
程序在运行时所需要的东西并不是都可以被共享,比如进程的变量。除此之外,进程有自己的栈空间,用于保存函数中的局部变量和控制函数的调用与返回。进程还有自己的环境空间,包含专门为这个进程建立的环境变量。
2、进程四要素
- 有一段程序供其执行,不一定专有,可以与其他进程共用
- 有进程专用的系统堆栈空间
- 有task_struct结构体分配给的PCB块
- 有独立的存储空间
这四条都是必要条件,缺一不可。如果只满足前两条而缺第四条,那就成了线程。如果完全没有用户空间,那就又成为了“内核线程”。而如果共享用户空间就称为“用户线程”。
3、关于进程的命令
-
ps命令:是最基本进程查看命令。使用该命令可以确定有哪些进程正在运行和正在运行的状态、进程是否结束、进程是否为僵尸进程,哪些进程占用了过多的资源。Ps是显示瞬间进程的状态,并不动态连续;如果想要对进程进行实时监控应该用top命令。
ps命令本身显示当前运行进程
-e参数本身是显示当前所有进程的不完整信息
-f参数本身显示当前运行进程的详细信息
-ef显示所有进程及详细信息
- kill命令:结束一个进程
4、进程的结构
1)PID:进程标识符
- 取值范围:2-32768,肯定是一个正整数
虽然进程的PID取值是从2开始的,但是根据fork的源码可以看出来它的真实值是从300开始的,300以前的作为内核的保留PID。而最大的范围根据源码看来是一个三目运算符的宏,如果sizeof(long)>4为真返回4*1024*1024=4194304,否则返回0x8000,换算为十进制就是32768。
开始为当前进程分配PID的时候,根据源码看来就是last+1。而如果pid>0x8000了,那么就直接返回300。
- 选择顺序:按顺序选择下一个未被使用的数字作为它的PID。如果一轮轮完,下一轮依然从2开始。
- PID=1:数字1是为特殊进程init保留的,init进程负责管理其他进程,是所有进程的祖先
2)进程表
Linux进程表就像一个数据结构,把当前加载在内存中的所有进程的有关信息保存在一个表中,包括进程的PID、进程的状态、命令字符串和ps命令输出的一些其他信息。操作系统通过进程的PID对它们进行管理,同时PID也是进程表的索引。
3)系统进程的当前状态表
5、fork
Linux下创建新进程的系统调用是fork。其定义如下:
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
该函数的每次调用都返回两次。父进程返回子进程的PID,子进程返回0。fork调用失败,返回-1。从fork的源码中可以看到,用来给子进程返回的寄存器直接被写入了0。返回0的理由是:一个进程只会有一个父进程,所以子进程可以调用getppid以获得其父进程的PID。
fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同。比如堆栈指针、标志寄存器的值。但是也有很多属性被赋予了新值,比如当前进程的PPID被设置成原进程的PID,信号位图被清除。
fork出的子进程的代码与父进程完全相同,子进程和父进程继续执行fork之后的代码,fork之后的同时它还会复制父进程的数据(堆数据、栈数据和静态数据),但是子进程获得的是父进程数据空间、堆栈的副本,父子进程不共享这些。但是因为fork之后常跟随exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,出现了“写时复制”。
写时复制时,内核此时并不复制整个进程空间,而是由父子进程共享,而且内核将它们的访问权限改变为只读。即只有任一进程对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据),然后内核只为修改区域的那块内存拷贝出一个副本,通常是虚存中的一页,从而使各个进程拥有自己的拷贝。
此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。除此之外,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。
fork复制进程后,父进程子进程交替执行,无法判断先后执行顺序。
6、exec系列系统调用
用fork函数创建子进程之后,需要在子进程中执行其他程序,即替换当前进程映像。此时子进程往往调用exec函数来执行另一个程序。当进程调用exec中的一种时,该进程执行的程序就被完全替换为新程序,新程序从其main函数开始执行。调用exec并不创建新进程,所以调用前后的进程ID并未改变。它只是替换了当前进程的正文、数据和堆栈。
#include<unistd.h>
extern char** environ;
int excel(const char* path,const char* arg,···);
int execlp(const char* file,const char *arg,···);
int execle(const char* path,const char* arg,···,char* const envp[]);
int execv(const char* path,char* const argv[]);
int execvp(const char* file,char* const argv[]);
int execve(const char* path,char* const argv[],char* const envp[]);
这6个函数中只有execve是系统调用,别的都是库函数,而且他们最终到要调用该系统调用。
7、do_fork()
说到do_fork(),就要涉及到fork的源码了。Linux的fork()、clone()、vfork()都是去调用do_fork()实现的。
do_fork()完成了创建中的大部分工作,它的定义在kernel/fork.c文件中。在它内部又调用了copy_process()函数,然后开始copy。这是创建进程的关键一步。
- path参数指定可执行文件的完整路径
- file参数可以接受文件名,如果file中包含‘/’,则将其视为路径,否则该文件的具体位置则在环境变量PATH中搜寻。
- arg接受可变参数
- argv接受参数数组,这些参数都会被传递给新程序(path或file指定的程序)的main函数。
- envp参数用于设置新程序的环境变量。如果未设置它,新程序将使用由全局变量environ指定的环境变量。
copy_process()函数原型:
- 在copy_process函数内重要的一步就是分配进程描述符(PCB),为新进程创建一个内核栈、thread_info和task_struct结构体,传current是因为要为子进程执行复制。而刚开始创建的值与当前进程的值是相同的。此时,子进程和父进程的描述符是完全相同的。
- 检查新创建的这个子进程之后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。
- 接着子进程开始与父进程进行区分,进程描述符内许多成员都被清0或设为初始值。进程描述符中的成员并不是继承来的,而主要是统计信息。进程描述符中的大多数数据都是共享的。
- 接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。
- copy_process()调用copy_flags()以更新task_struct的数据成员。表名进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0,表名进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
- 调用get_pid()为新进程获取一个有效的PID。
- 根据传递给clone()函数的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则这些资源对每个进程来说都是不同的,因此被拷贝到这里。
- 让父进程和子进程平方剩余的事件片。
- 最后,copy_process()作扫尾工作并返回一个指向子进程的指针。
如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行,因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。如果父进程首先执行的话,有可能会开始向地址空间写入。
8、wait()
当一个进程正常或者异常终止时,内核就向其父进程发送SIGHLD信号。因为子进程终止是个异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数。
当用fork启动一个子进程时,子进程就有了自己的生命周期并将独立运行。可以通过在父进程中调用wait函数让父进程等待子进程结束。
如果调用wait或waitpid的进程可能户发生什么情况:
- 如果其所有子进程都还在执行,则阻塞。
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
- 如果它没有任何子进程,则立即出错返回。
如果进程由于接收到SIGHLD信号而调用wait,则wait会立即返回,但是如果在任意时刻调用wait,则进程可能会阻塞。
#include<sys/wait.h>
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid,int *statloc,int options);
这两个函数的区别如下:
- 在一个子进程终止前,wait使其调用者(父进程)阻塞。而waitpid有一个选项,可以使其不阻塞。
- waitpid并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。
如果一个子进程已经终止,并且是一个僵尸进程,则wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如果调用者有多个子进程,则在其一个子进程终止时,wait即立即返回。因为wait返回终止子进程的进程ID。
- 参数stat_loc是一个整型指针。如果它不是一个空指针,其中存放终止进程的退出状态,即子进程的main函数返回的值或子进程exit的函数的退出码。如果不关心终止状态,可将其置为NULL
9、进程终止
有8种方式使进程终止,其中5种为正常终止,3种为异常终止。
- main函数返回
- 调用exit
- 调用_exit或_Exit
- 最后一个线程从其启动例程返回
- 最后一个线程调用pthread_exit
异常终止的3种方式:
- 调用abort
- 接到一个信号并终止
- 最后一个线程对取消请求做出响应