与进程相关的基础知识,可以移步先去看看这篇文章——《进程基础整理》。
进程的状态:
一个进程随着执行地推进改变着自己的状态(state)。进程的状态由进程在当前所执行的动作有关。每一个进程的状态都应是以下所列举的状态中的一种:
- 新建(New):创建一个进程
- 运行(Running):执行指令
- 等待(Waiting):进程等待某件事的发生(例如:I/O的完成、信号的接收)
- 就绪(Ready):进程在就绪队列中等着被CPU调度,以能够分派到处理器
- 终止(Terminated):进程执行结束
1.fork函数
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork系统调用的返回:若成功,父进程中返回子进程的PID,子进程中返回0;若出错则返回-1。(fork出错可能有两种原因:一是当前的进程数已经达到了系统规定的上限,这是errno上的值被设置为EAGAIN;二是系统内存不足,这是errno的值被设置为ENOMEM)。
fork系统调用的作用是复制一个进程。当一个进程调用它,完成后就出现两个一模一样的进程,我们也由此得到了一个新进程。由fork创建的新进程被称为子进程(child process),而将原来的进程称为父进程(parent process)。子进程是父进程的一个拷贝,即子进程从父进程得到了数据段和堆栈段的拷贝,这些需要分配新的内存;而对于只读的代码段,通常使用共享内存的方式访问。
fork函数的主要用途是:
i)一个进程希望复制自身,从而父子进程能同时执行不同段的代码
ii)进程想执行另外一个程序
fork函数被调用一次,但返回两次(这样的函数在linux中只有少数几个)。两次返回的区别是子进程的返回值是0,而父进程的返回值是新的子进程的进程ID。将子进程的进程ID返回给父进程的理由是:因为一个进程的子进程可以多于一个,所以没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得父进程的进程ID(进程ID0总是由交换进程使用,所以一个子进程的进程ID不可能为0)。
fork返回后,子进程和父进程都从fork函数的下一条语句开始执行。
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。如果要求父子进程之间相互同步,则要求某种形式的进程间通信。
在linux中,创造一个新进程的方法只有这一个——fork系统调用。
对fork有深入理解,可以参考这篇文章——《一个fork的面试题》。
2.exec函数
linux使用exec函数族来执行新的程序,以新的子进程来完全替代原有的进程。
实际上,在linux中并不存在一个exec的函数形式,exec是一个函数族,指的是一组函数,共6个,分别是:
#include <unistd.h>
int execl(const char *pathname, const char *arg, …);
int execlp(const char *filename, const char *arg, …);
int execle(const char *pathname, const char *arg, …, char *const envp[]);
int execv(const char *pathname, const char arg[]);
int execlp(const char *filename, const char arg[]);
int execle(const char *pathname, const char arg[], …, char *const envp[]);
六个函数返回:若成功则无返回值,若出错则返回-1。
事实上,其中只有execve才是真正意义上的系统调用,其他都是在此基础上经过包装的库函数。exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何linux下可执行的脚本文件。
与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈段都已经被新的内容所取代,只是进程ID等一些表面上的信息仍保持原样,颇有点神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。
现在我们就能知道 linux是如何执行新程序的?(fork函数调用之后,子进程是父进程的一个 完全拷贝,但是fork之后一般都会调用exec函数族......)
i)每当有进程认为自己不能为系统和拥护做出任何贡献了,它就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生
ii)如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样。
事实上第二种情况被应用的更普遍,以至于linux专门为其做了优化,我们已经知道,fork会将调用进程的所有内容原封不动地拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后马上就调用了exec,这些辛辛苦苦拷贝来的东西又会被立刻抹掉,这看起来非常不划算,于是人们设计了一种写时复制(Copy-On-Write,COW)技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec,它就不会白白做无用功了也就提高了效率。
3.exit函数
#include <stdlib.h>
void exit(int status);
还有另一相似的系统调用_exit(),对于这两函数的差别可以参考——
exit()和_exit()的差别
4.wait和waitpid函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
两个函数的返回:若成功则返回进程ID,若出错则返回-1。
pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
5.一个简单的示例程序
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
int main(){
pid_t pid;
pid = fork();
if (pid < 0){/*error occurred*/
fprintf(stderr,"Fork Failed!");
exit(-1);
}
else if (pid == 0){/*Child Process*/
execlp("/bin/ls","ls",NULL);
}
else{/*Parent Process*/
/*parent will wait for the child to complete*/
wait(NULL);
printf("Child Complete!");
exit(0);
}
}
6.进程的一生
首先,随着fork的成功执行,一个新的子进程诞生,但此时的它还只是父进程的一个克隆,从父进程哪里得到了数据段和堆栈段的拷贝。然后随着exec,新进程脱胎换骨,离家独立,开始独立执行一个全新的程序,并完全替代了原有拷贝的父进程的数据段和堆栈段数据。人有生老病死,进程也是一样,它可以是自然死亡,即运行到main函数的最后一个“}”,从容地离我们而去;也可以是自杀,自杀有两种方式,一种是调用exit函数,一种是在main函数中使用return,无论是哪一种方式,它都可以留下遗书,放在返回值里保留下来;它甚至还可以被谋杀,被其他的进程通过另外一些方式来结束它的生命。
进程死掉之后,会留下一具僵尸,称为“僵尸进程”,waitpid充当殓尸工,把僵尸推过去火化,使其最终归于无形。
这就是进程完整的一生。
参考资料:《Linux C 编程 从入门到精通》