为何经常要把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;
}
}
这段程序有几个要点:
fork()
函数是linux下特有的,所以这一段在windows的Clion里报找不到fork()函数,但实际上放到linux下可以正常编译运行。
- 在调用fork()的位置,新的进程会接着当前的程序继续往下执行,而涉及到的变量,会复制一份,不会在进程间互相影响。
- 对于这个程序来说,分支的一瞬间,立刻就判断进程是子进程还是父进程,如果是子进程就显示进入子进程,然后把count++。
- 如果是父进程,注意会立刻继续循环一次,再去开启一个子进程。而子进程会打印他们各自的count之后,遇到return 0就结束了。
- 所以fork函数很形象,就是在调用的时候分叉出去一个分支,然而要小心判断逻辑。这里如果不小心没有加上return 0;子进程也会立刻进入开启进程的循环,就无穷无尽了。
所以一般就让主进程不断开启子进程,而子进程就去执行exec,立刻会被替换掉然后完后结束,就可以满足开始的时候执行多个命令的需求了。
当然父子进程以后估计在UNIX环境高级编程中肯定解释的更详细了,这里先理解怎么用就可以了。