[APUE] 实战1:实现system
受《APUE》第八章进程控制启发,想自己实现一个可以支持多个命令行参数,但尚不具备信号处理能力的
system(1)
。巩固一下字符串处理以及进程控制的三个元函数fork(), exec..(), wait()
的使用。进而为后期实现shell
做准备。
一、任务要求
1. 整体目标
实现函数原型:void system(const char* cmd)
使其能够调用 shell
执行 cmd[]
中的指令,就像直接在命令行中键入 cmd[]
一样
2. 细节说明
指令长度
- 一行命令最多有 15 15 15 个参数
- 每个参数最长为 20 20 20 字节
输入格式
cmd[]
为有效的,可在shell
中直接执行的一行命令,例如ps -a
- 相邻的两个参数由一个或多个空格分隔,即
ls -a
与ls -a
等价
二、任务难点
1. 字符串处理
- 对字符串按空格进行拆分
- 运用
<string.h>
中的函数,例如memcpy(3) strlen(1) strtok(2)
2. 辨析各类指针
- 指针数组:
char* argv[]
- 字符串:
char* a
与char a[]
的区别 - 内存分配:对
memset()
的返回值需指定的类型
3. exec函数族
该函数族在<unistd.h>
头文件中,共有
6
6
6 个函数。网上各类资源对于该函数的说明都不够全面。在调用的时候,极容易困惑、出错。我采用的是 int execv(2)
。而在原书中,Stevens 先生采用的是 int execv(2+)
三、实现过程
1. 分析 execv(2)
函数原型
int execv(const char* pathname, char* const argv[])
工作原理
当进程调用该函数时,改进程执行的程序完全替换为指定的新程序。即当前进程的正文段、数据段、堆和栈段均被替换,而进程ID号不变。
因而为了使其能够正常返回至原有的运行逻辑,需:
- 先
fork(0)
一个子进程 - 由该子进程
execv(2)
相应的功能函数 - 而父进程
wait()
子进程结束后清理
这三步也是一般程序涉及进程控制的必要步骤,因而被称为进程控制原语
参数分析
-
pathname[]
将被装入的程序的绝对路径,例如
/bin/ps
-
char* argv[]
指针数组,每个元素为
char*
类型的指针,指向每个命令行参数。特别要注意的是,最后一个元素需指向空指针(char*)0
表示已读取至最后一个参数。例如,命令ps -a
拆分至argv[]
可得:argv[0] = "ps"; argv[1] = "-a"; argv[2] = (char*)0;
2. 处理字符串
① 目标
分割命令行参数并装入不同的内存单元
② 步骤
分割命令行参数
一开始是想自己实现的,但在实现的过程中在 <string.h>
中发现了不少有用的函数,其中就有一个直接满足了我的需求。下面来介绍一下这个“冷门函数”:
-
函数原型
char* strtok(char* str, const char* delim)
-
返回值
首个两连续分隔符之间的非空字符串。若无匹配项,则返回
NULL
-
参数分析
str[]
:待分割的字符串,首次调用时填写,称之为“绑定”,之后用NULL
来指代分割后剩下的字符串。特别要注意,传入的str
会被修改。delim[]
:分隔符
-
样例
// test_strtok.c #include <string.h> #include <stdio.h> int main () { char a[] = "I#Love##Programming#In Unix Environment#"; printf("a = %s\n\n", a); char* tok; tok = strtok(a, "#"); while (tok != NULL) { printf("tok = %s\n", tok); printf("a = %s\n\n", a); tok = strtok(NULL, "#"); } }
例如以 “I#Love##Programming#In Unix Environment#” 作为待分割的字符串,以 “#” 作为分隔符,得到如下结果:
a = I#Love##Programming#In Unix Environment# tok = I a = I tok = Love a = I tok = Programming a = I tok = In Unix Environment a = I [Run In Ubuntu 16.04 LTS]
装入不同的内存单元并由 argv[]
指向
考虑要对字符串完全分割,所以要在 while
中不断调用 strtok(2)
。又要用不同的指针指向分割后的字符串,所以需在循环体内声明指针并赋值。因此,处理字符串部分的函数可作出:
// Split cmd[] to argv[]
char* fstArg = strtok(cmdcpy, " ");
argv[argc++] = fstArg;
while (1)
{
char* remArgs = strtok(NULL, " ");
if (remArgs == NULL) break;
argv[argc++] = remArgs;
}
argv[argc++] = (char*)0;
细心观察的朋友会发现,strtok(2)
传入的第一个参数是 cmdcpy[]
而不是 cmd[]
。因为前面说到,strtok(2)
会改变参数的值,而 cmd[]
又是常量,不可改变,因此作了一个它的副本 cmdcpy[]
,手动按值传递
四、源代码及运行示例
1. 源代码
第一版支持多个命令行参数,但不支持信号处理的 system(1)
的具体实现给出如下:
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
const int MAX_ARGC = 15;
const int MAX_ARGLEN = 30;
/* Function
Execute the command
*/
int system(const char* cmd)
{
// Init char* argv[]
char* cmdcpy = (char*)malloc(sizeof(char) * MAX_ARGLEN);
memcpy(cmdcpy, cmd, strlen(cmd)+1);
int argc = 2, arglen = 0;
char* argv[MAX_ARGC];
char arg0[] = "sh";char arg1[] = "-c";
argv[0] = arg0;argv[1] = arg1;
// Split cmd[] to argv[]
char* fstArg = strtok(cmdcpy, " ");
//printf("fstArg = %s\n", fstArg);
argv[argc++] = fstArg;
while (1)
{
char* remArgs = strtok(NULL, " ");
if (remArgs == NULL) break;
argv[argc++] = remArgs;
}
argv[argc++] = (char*)0;
// Invoke fork(0), execv(2), wait(1)
if (fork() == 0)
{
printf("bash will execute\n");
execv("/bin/sh", argv);
printf("bash completed\n");
}
int chldID;
if ( (chldID = wait(NULL)) > 0)
{
printf("chldProcess %d terminated successfully\n", chldID);
return 0;
}
return -1;
}
2. 运行示例
测试函数
int main(int argc, char const *argv[])
{
system("ps -a");
system("ls -l");
return 0;
}
运行结果
PID TTY TIME CMD
9274 ? 00:00:00 systemd
9278 ? 00:00:00 (sd-pam)
9342 ? 00:00:00 gnome-keyring-d
10003 ? 00:00:00 sh
10007 ? 00:00:00 zeitgeist-daemo
10014 ? 00:00:00 zeitgeist-fts
10018 ? 00:00:00 zeitgeist-datah
10028 ? 00:00:00 bash
10036 ? 00:00:00 impsys
10037 ? 00:00:00 sh
10038 ? 00:00:00 ps
apue.2e
impsys
impsys.c
NetProg
qtProj
unpv13e
xv6-public
cmdcpy = ps -a
chldProcess 10037 terminated successfully
cmdcpy = ls -l
chldProcess 10039 terminated successfully
[Finished in 1.1s]