前文中我们讲述了进程的退出码,和通过位运算获得进程退出码的方法。本文中我们继续讲述通过宏的方法获得子进程退出码,以及进程程序替换的相关内容。
可以使用下面的一些宏方法获得进程退出码的内容。
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
进程程序替换
创建子进程的目的:为了让子进程执行特定的任务(让子进程执行部分父进程的代码;让子进程想执行一个全新的程序代码),因此需要有程序替换这样的操作。
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
替换函数
通过查看man手册可以看到替换函数的基本信息:
上图中的...被称为可变参数列表,可以让我们像一个C函数传递任意个数的参数。除了上述六种之外还有一个替换函数:
int execve(const char *path, char *const argv[], char *const envp[]);
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值
命名理解
这些函数原型虽然看起来比较的混乱,但是都有着规律:
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
下面就来简单地举一下例子:
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
上述替换函数中在手册中会发现只有execve在手册第二节,因为这个函数是真正的函数调用,其余的五个函数都是调用的execve,通过这些封装我们可以更灵活的去使用程序替换函数。
我们可以简单地使用之前学习过的内容来实现一个简易的shell:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAX 1024
#define ARGC 64
#define SEP " "
int split(char* commandstr, char* argv[])
{
assert(commandstr);
assert(argv);
argv[0] = strtok(commandstr, SEP);
if (argv[0] == NULL) return -1;
int i = 1;
while ((argv[i++] = strtok(NULL, SEP)));
//int i = 1;
//while(1)
//{
// argv[i] = strtok(NULL, SEP);
// if(argv[i] == NULL) break;
// i++;
//}
return 0;
}
void debugPrint(char* argv[])
{
for (int i = 0; argv[i]; i++)
{
printf("%d: %s\n", i, argv[i]);
}
}
int main()
{
while (1)
{
char commandstr[MAX] = { 0 }; // 命令行
char* argv[ARGC] = { NULL };
printf("[zyq@mymachine currpath]# "); // 这里的每一个字段都可以通过系统调用获取 [用户名@主机名当前路径]
fflush(stdout); // 没有换行,需要对缓冲区刷新使上述的命令行字段能够显示出来
char* s = fgets(commandstr, sizeof(commandstr) - 1, stdin); // 获取当前的以空格为间隔的字符串 // 在这里最好对sizeof减去1;fgets本来会添加\0,但是最好要添加上,因为编写系统代码的时候,有的是C的接口,有的是操作系统的接口,C接口会减但是系统接口不会,代码会比较割裂
assert(s); // 为NULL时获取失败
(void)s; // 保证在release方式发布的时候,因为去掉assert了,所以s就没有被使用,而带来的编译告警, 什么都没做,但是充当一次使用
// abcd\n\0
commandstr[strlen(commandstr) - 1] = '\0'; // 在我们输入的时候会自动读取回车,但是这是我们不需要的,因此需要去除
// "ls -a -l" -> "ls" "-a" "-l"
int n = split(commandstr, argv); // 通过空格分隔指令
if (n != 0) continue; // 如果切失败了什么都不做
//debugPrint(argv);
// version 1
pid_t id = fork();
assert(id >= 0);
(void)id;
if (id == 0)
{
//child
execvp(argv[0], argv);
exit(1);
}
int status = 0;
waitpid(id, &status, 0);
//printf("%s\n", commandstr);
}
}