C再学习 07 – 系统调用和父子进程

作者 柚爸

为何经常要把C和操作系统联系起来,因为一来操作系统就是C写的,二来C要发挥作用,由于比较底层,与操作系统的直接交互非常多。

这次要看看之前看C现代方法时候从来没有接触过的内容,也就是进程和系统调用了。

系统调用初步

stdlib.h中的system()函数就是系统命令行调用。

这个参数接受一个字符串,这个字符串就会相当于送到系统的终端命令中执行。

比如system("dir D:")在windows下边就是执行一个列出D盘目录的命令。

看一个简单的小程序:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

char *now(){
    time_t time1;
    time(&time1);

    return asctime(localtime(&time1));
}

int main(int argc, char *argv[]) {

    char comment[80];
    char cmd[120];

    scanf("%[^\n]", comment);

    sprintf(cmd, "echo %s >> report.log", comment);

    printf("%s\n", cmd);

    system(cmd);

    return 0;
}

scanf读入一行字符串,不带有换行符,然后将其通过sprintf,和其他部分拼接成一个命令,打印到一个字符串中,然后把这个字符串传递给system函数当做命令执行。

如果有过SQL,或者JS经验的人都知道,这种命令不加限制非常危险,可以执行任何程序。

所以肯定还有其他方法。system()是系统内核提供的函数,这个函数在编译的时候不会放到我们自己编写的可执行文件中。

exec()与进程

进程就是一个正在运行的程序的抽象。在Linux中可以通过ps -ef命令来查看进程,进程的PID(process identifier)就是唯一标识。

exec所在的头文件是unistd.h

exec()实际上是允许其他程序来替换当前进程,同时可以指定使用的命令行参数和环境变量。新程序启动之后的PID和老程序一样。

exec函数有很多不同名称版本,不同版本的函数可接受的参数不同。

exec()的版本

exec()的不同版本实际上是exec??(),其中??可以是一个字符也可以是两个字符。如果是一个字符,只能是l或者v,如果是两个字符,则第一个字符是l或者v,第二个是p或者e。其含义如下:

字符 意义
l 使用参数列表
v 使用参数数组/向量
p 从PATH中查找程序
e 带有环境变量

其中的p和e可以省略。

exec()的参数

在之前的版本中知道,有两个版本,一个使用参数列表,一个使用参数数组/向量。

先来看列表版本的参数:

execl("/home/jenny/shout", "/home/jenny/shout", "-l", "cry", NULL)

红色部分是第一个参数,所有exec的第一个参数都是要执行的程序的完整路径名,特别的对于execlp,这个参数是程序的名称,会到PATH中去查找。

后边绿色的部分是参数列表,其中强制第一个元素是程序名,所以列表版的exec的前两个参数看上去相同。

再之后就是依次传递的命令参数,最后必须以一个NULL结尾,表示参数结束。

知道了基础的execl,就可以扩展其他的内容了。

如果带上e,则还可以传递环境变量,环境变量是一个字符串数组,里边每一个元素用property=value的形式存放,也必须以NULL结束。看一个例子:

char *env[] = {"JUICE=cony", "DRINK=cola", NULL};
execle("/usr/cmd/hello", "/usr/cmd/hello", "-ft", "saner.txt", NULL, env);

而数组,向量版就更简单了,参数列表扔到一个数组里就可以了:

execve("/usr/cmd/hello", param_args, env);

当然,参数数组也要以NULL结尾。

PATH版无需传入路径,只要PATH中能找到这个名称的程序即可:

execvp("dir", param_args);

检测错误代码

在我们的程序运行的时候,也会产生一个进程。我们的程序中执行exec的效果就是,将当前的程序的进程替换成exec中执行的程序的进程。

所以如果不使用子进程,则当前进程的程序在之后不会执行,这点和system()是不同的。可以使用errno.h,在exec执行失败的时候获取错误码,然后通过strerror(errno)来打印错误信息:

int main(int argc, char *argv[]) {

    char any[80];

    printf("input something: ");
    fgets(any, 80, stdin);

    execlp("ipconfig", "ipconfig", "all" ,NULL);

    puts(strerror(errno));
    puts("failed to exec");

}

如果有错误,就会打印出来。

获取环境变量

每一个进程都会有环境变量。比如在cmd或者bash下输入set,会列出当前终端窗口进程的所有环境变量。在C里可以通过stdlib.h中的getenv("key")来获取环境变量的值。

int main(int argc, char *argv[]) {

    printf("USERNAME is %s\n", getenv("USERNAME"));

}

这个会打印出当前的用户名。注意,环境变量在Linux下是大小写敏感的。在windows下则大小写不敏感。

fork()与子进程

刚才提到,exec执行的时候,当前的进程就会结束,替换成exec执行的进程。如果想要依次执行多个命令。就不能简单的直接写exec。

系统调用函数fork()会创建一个新的进程,是当前进程的子进程,变量和值都相同,只是PID不同。

进程需要明白自己是子进程还是父进程,可以调用fork(),会向子进程返回0,向父进程返回大于零的一个进程号,可以当做true来判断。如果返回-1,则说明在启动子进程的时候出了问题。

比如我现在调用了一次fork(),就有两个进程了,于是可以进行一个判断,是子进程就去运行exec光荣的将自己替换成新的命令,如果是父进程,则不执行。

一个典型的判断例子是:

int main(int argc, char *argv[]) {

    int i = 0;
    pid_t pid;

    int count = 0;

    printf("Current Process  Id = %d \n", getpid());
    for (; i < 3; i++) {
        if((pid = fork()) < 0){
            printf("Error");
        } else if (pid == 0) {
            count++;
            printf("进入子进程,当前进程 ID = %d\n" ,getpid());
        } else {
            printf("我是父进程,进程ID= %d\n", getpid());
                continue;
        }

        printf("当前进程ID = %d, count=%d\n", getpid(), count);
        return 0;
    }

}

这段程序有几个要点:

  1. fork()函数是linux下特有的,所以这一段在windows的Clion里报找不到fork()函数,但实际上放到linux下可以正常编译运行。
  2. 在调用fork()的位置,新的进程会接着当前的程序继续往下执行,而涉及到的变量,会复制一份,不会在进程间互相影响。
  3. 对于这个程序来说,分支的一瞬间,立刻就判断进程是子进程还是父进程,如果是子进程就显示进入子进程,然后把count++。
  4. 如果是父进程,注意会立刻继续循环一次,再去开启一个子进程。而子进程会打印他们各自的count之后,遇到return 0就结束了。
  5. 所以fork函数很形象,就是在调用的时候分叉出去一个分支,然而要小心判断逻辑。这里如果不小心没有加上return 0;子进程也会立刻进入开启进程的循环,就无穷无尽了。

所以一般就让主进程不断开启子进程,而子进程就去执行exec,立刻会被替换掉然后完后结束,就可以满足开始的时候执行多个命令的需求了。

当然父子进程以后估计在UNIX环境高级编程中肯定解释的更详细了,这里先理解怎么用就可以了。