子进程和exec族函数的使用(Linux C)


一 子进程的概念和创建

关于进程相关概念,在计算机操作系统课程中已经详细介绍了,小编就不再班门弄斧。

但想强调的是:进程是系统进行资源调度和分配的基本单位。程序和进程之间不存在一一对应的关系,一个程序可以对应多个的进程,一个进程也可以对应多个程序;程序是静态的,存储在计算机磁盘中,进程是动态的,存在生命周期,有“生老病死”;我们通常把进程看作为是程序在内存中的镜像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;
}

函数运行结果:
ans
其实大家对于 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;
}
  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值