一 子进程的概念和创建
关于进程相关概念,在计算机操作系统课程中已经详细介绍了,小编就不再班门弄斧。
但想强调的是:进程是系统进行资源调度和分配的基本单位。程序和进程之间不存在一一对应的关系,一个程序可以对应多个的进程,一个进程也可以对应多个程序;程序是静态的,存储在计算机磁盘中,进程是动态的,存在生命周期,有“生老病死”;我们通常把进程看作为是程序在内存中的镜像image。
我们通过fork()函数创建子进程,fork()函数通过复制当前进程产生一个新的进程,所以绝大部分的内容是相同的,父子进程他们运行在不同的内存空间。针对与父子进程之间的不同,我们直接引用man手册原文。
The child process is an exact duplicate of the parent process except for the following points:
- The child has its own unique process ID, and this PID does not match the ID of any existing process group (setpgid(2)) or session. (子进程有自己独立的进程id)
- The child does not inherit its parent’s memory locks (mlock(2), mlockall(2)). (没有继承父进程的内存锁)
- Process resource utilizations (getrusage(2)) and CPU time counters (times(2)) are reset to zero in the child. (系统资源利用率和CPU计数器被重置为0)
- The child does not inherit semaphore adjustments from its parent (semop(2)). (不会继承父进程的信号量调整)
…等等,还有很多,大家可以man fork查看。
关于子进程,有两种子进程需要介绍:孤儿进程、僵尸进程
孤儿进程:父进程死亡,子进程依旧存在,此时子进程就变为孤儿进程,会被init进程 (1号进程) 收养。
需要注意的是小编在代码测试时发现:父进程死亡后,孤儿进程的新父进程的pid并不是1。
这是因为我们当前使用的终端是图形化命令终端,我们在当前shell中创建的所有进程都是当前shell进程的子进程,所以孤儿进程最后被shell进程收养的,而不是init进程 (这里需要说明,当计算机开机时,内核kernal只建立一个init进程,剩下的所有进程都是init进程通过fork函数创建的,所以init进程是所有进程的父进程)
若大家想要孤儿进程被init进程收养,需要进入字符界面终端(Ctrl Alt F1到F6)均可,之后再次运行程序,孤儿进程的父进程显示的pid就是1。若是想要退回到原来的图形化界面:输入 startx 或者Ctrl Alt F7 即可。但是小编当时尝试了好几种方法,都回不去,最后只好重启电脑才行,各位看官谨慎操作。
僵尸进程:若子进程先于父进程结束,但是父进程没有读取子进程的运行结果和回收子进程的资源 (就是子进程在内存中的PCB process control block 进程控制块)那么子进程就是僵尸进程。
其实父进程结束后,僵尸进程还是由init进程来“收尸”,这本来是没什么的。但若父进程是一个for循环呢?,假设for循环100000次,甚至更多,每一次循环创建一个子进程,但是父进程迟迟没有结束,init想“收尸”也不行,这样内存中就会有大量的子进程PCB,造成大量的内存泄漏。这显然是非常不好的!
二 fork()函数举例和父子进程执行
现在我们假设要创建10个进程,每一个进程睡眠10秒钟后退出,请用代码实现!
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int pid, x = 0;
for (int i = 1; i <= 10; i++) {
if ((pid = fork()) < 0) {
perror("fork()");
continue;
}
if (pid == 0) { //子进程
x = i;
break;
}
}
printf("this is the %dth child process\n", x);
sleep(10);
return 0;
}
函数运行结果:
其实大家对于 if(pid == 0) {} else {} 语句很熟悉,不就是条件分支判断吗,但这里判断的是什么呢?
平时 if else判断, 若if条件满足,就执行 if 对应的语句,否则就执行else对应的语句,总的来说,就是两者选一执行,但是这个程序的表现上来看,if 和 else 对应的片段都执行了,为什么会这样?此外还有就是父子进程的执行先后顺序是什么呢?
准确的来说,这里是判断当前的进程是父进程还是子进程!
这还要从fork()函数的一次调用、两次返回说起,父进程在执行到fork函数后,系统会复制一个和父进程一样的子进程(虽说是父子进程,但实际上更像是兄弟关系,fork意为分叉,可以理解为餐具叉子,两者之间是并列的),这两个进程共享代码段,对于父进程fork函数返回的是子进程的pid,所以面对if else语句,它执行的自然是else部分,子进程fork函数返回的是0,所以执行的是 if 对应的语句。两个进程一个执行 if 一个执行 else,所以看起来就像 if 没有起到条件分支的作用。
再就是父子进程的执行顺序,其实大家就把他们看成两个没有关系的进程,他们的执行顺序是没有联系的。具体哪一个进程先运行,这取决于操作系统系统的调度算法。
若我们想要让哪一个进程先执行,那我们可以把另一个进程sleep睡眠若干秒 (sleep睡眠其实就是将该程序挂起)。
一般来说,还是子进程先于父进程执行,因为我们前面说过孤儿进程和僵尸进程的产生,这样显然是不好的。
三 exec族函数
一般来说创建子进程是和exec函数是搭配使用的,man手册中的原文描述是:
The exec() family of functions replaces the current process image with a new process image. (函数会用一个新的进程镜像替换当前进程镜像),换句话说,就是在该子进程中执行一个可执行文件,这个可执行文件可以是二进制可执行文件也可以是脚本文件。
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
注意:只有execvpe()函数是真正意义上的系统调用,其他的函数都是经过包装的库函数。
我们来看一下函数的参数:
(1) file或path 指的是可执行文件的路径,若可执行文件中没有出现"/",那么系统就会去PATH路径中查找这个可执行文件。
(2) arg 指的是可执行文件对应的参数, 例如gcc这个可执行文件,后面肯定要接文件名作为参数。细看这6个参数,我们会发现,前三个函数名是以execl开头的,后三个是execv开头的,前者开头的函数,参数都是一个一个的列出来的,最后需要写一个NULL结尾。后面的函数是一个字符串数组,其中每一个元素就是一个参数字符串,同样,数组最后一个元素必须是NULL。
关于exec一组函数的解析 若想要了解更多 请点击我
现在我们写一个程序,要求程序运行时,打开vim创建一个文件(若存在则打开)编辑完退出后,会编译该文件,最后运行程序。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc - 2) {
printf("Usage: %s filename\n", argv[0]);
exit(1);
}
pid_t pid;
int status = 0;
pid = fork();
if (pid == 0) { //打开vim
execlp("vim", "vim", argv[1], NULL);
}
wait(&status); //等待子进程结束
if (!WIFEXITED(status)) { //程序非正常结束
perror("open vim filed");
exit(1);
}
pid = fork();
if (pid == 0) { //编译文件
int len = strlen(argv[1]);
char name[10] = {0};
if (argv[1][len - 2] == '.' && argv[1][len - 1] == 'c') { //.c文件
execlp("gcc", "gcc", argv[1], "-o", "tmp", NULL);
} else { //.cpp文件
execlp("g++", "g++", argv[1], "-o", "tmp", NULL);
}
}
wait(&status);
if (!WIFEXITED(status)) {
perror("compile file filed");
exit(1);
}
pid = fork();
if (pid == 0) { //执行文件
char path[100] = {0};
getcwd(path, 100);
strcat(path, "/tmp");
//printf("path = %s\n", path);
execl(path, "./a.out", NULL);
}
wait(&status);
if (!WIFEXITED(status)) {
perror("execute file filed");
exit(1);
}
return 0;
}