LV05-01-进程-03-在进程中执行程序

本文主要是进程——在进程中执行程序相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

很多时候,我们希望父子进程可以执行不同的程序,下边就来看一看如何转而执行其他的程序吧。

一、exec函数族

1. 七个函数

exec 函数族共有 7 种不同形式的函数:

1
2
3
4
5
6
7
8
9
int execl(const char *pathname, const char *arg, ... /* (char  *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);

int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

int execve(const char *pathname, char *const argv[], char *const envp[]);

【命名规律】 exec[l or v][p][e]

e参数必须带环境变量部分, 环境变量部分参数会成为执行exec函数期间的环境变量。
l命令参数部分必须以 " , " 相隔, 最后1个命令参数必须是NULL,表示结束。
v命令参数部分必须是1个以NULL结尾的字符串指针数组的头部指针。
p执行文件部分可以不带路径, exec 函数会在 $PATH 中找。

【参数分类】 exec 函数里的参数大概可以分成 3 个部分:执行文件部分、命令参数部分和环境变量部分。

2. execl开头

2.1 execl()

2.1.1 函数说明

在 linux 下可以使用 man 3 execl 命令查看该函数的帮助手册。

1
2
3
#include <unistd.h>
extern char **environ; /* 手册上有写,但是似乎不写也没什么 */
int execl(const char *pathname, const char *arg, ... /* (char *) NULL */);

【函数说明】该函数将新程序加载到某一进程的内存空间,将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main() 函数开始执行。

【函数参数】

  • pathname : const char * 类型,参数 pathname 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
  • arg : const char * 类型,传递给执行的程序的参数列表,使用可变参数形式传递,本质上是多个字符串,以 NULL 结尾。

【返回值】返回值为 int 类型,调用成功将不会返回;失败将返回 -1 ,并设置 errno 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
execl("./newTest", "./newTest", "Hello", "World", NULL, "!");

【注意事项】 none

2.1.2 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#include <unistd.h>
int main(int argc, char *argv[])
{
if(execl("/bin/ls", "ls", "-a", "-l", "./", NULL) < 0)
{
perror("execl");
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
总用量 198
drwxrwxrwx 1 root root 4096 5月 23 11:49 .
dr-xr-xr-x 1 root root 4192 5月 23 11:49 ..
drwxrwxrwx 1 root root 4096 3月 1 20:41 1Pictures
# 剩下的省略 ...

2.2 execlp()

2.2.1 函数说明

在 linux 下可以使用 man 3 execlp 命令查看该函数的帮助手册。

1
2
3
#include <unistd.h>
extern char **environ; /* 手册上有写,但是似乎不写也没什么 */
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);

【函数说明】该函数借助 PATH 环境变量将新程序加载到某一进程的内存空间,然后从新程序的 main() 函数开始执行,与 execl 函数功能一样,只是该函数不需要给出新程序的路径。

【函数参数】

  • file : const char * 类型,参数 file 指向需要载入当前进程空间的新程序的名称,不需要给出路径,但是要保证新程序路径存在于环境变量 PATH 中。
  • arg : const char * 类型,传递给执行的程序的参数列表,本质上是多个字符串,以 NULL 结尾。

【返回值】返回值为 int 类型,调用成功将不会返回;失败将返回 -1 ,并设置 errno 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
execlp("newTest", "newTest", "Hello", "World", NULL, "!"); /* newTest 路径需要存在于 $PATH 中 */

【注意事项】相对于 execl , execlp 仅要求提供新程序的名称即可,但是需要保证新程序路径存在于环境变量 $PATH 中。

2.2.2 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#include <unistd.h>
int main(int argc, char *argv[])
{
if(execlp("ls", "ls", "-a", "-l", "./", NULL) < 0)
{
perror("execlp");
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
总用量 198
drwxrwxrwx 1 root root 4096 5月 23 13:17 .
dr-xr-xr-x 1 root root 4192 5月 23 2022 ..
drwxrwxrwx 1 root root 4096 3月 1 20:41 1Pictures
# 剩下的省略 ...

2.3 execle()

2.3.1 函数说明

在 linux 下可以使用 man 3 execle 命令查看该函数的帮助手册。

1
2
#include <unistd.h>
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);

【函数说明】该函数将新程序加载到某一进程的内存空间,将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main() 函数开始执行。

【函数参数】

  • pathname : const char * 类型,参数 pathname 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
  • arg : const char * 类型,传递给执行的程序的参数列表,本质上是多个字符串,以 NULL 结尾。
  • envp[] : char *const 类型数组,即是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value 。

【返回值】返回值为 int 类型,调用成功将不会返回;失败将返回 -1 ,并设置 errno 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL};
execle("./newTest", "./newTest", "Hello", "fanhua", NULL, env_arr);

【注意事项】

(1)对 execle() 的成功调用将永不返回,而且也无需检查它的返回值,一旦该函数返回,就表明它发生了错误。

(2)当使用 execle() 传入命令行及环境变量参数时,都是遇到 NULL 就结束,新执行程序的命令行参数的 argv[0] 由调用该新程序的程序传入的第一个参数决定。

2.3.2 使用实例

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

#include <unistd.h>
int main(int argc, char *argv[])
{
int ret = 0;
char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL, "Fanhua=1"};

printf("This is test.c!\n");
ret = execle("./newTest", "./newTest", "Hello", "fanhua", NULL, env_arr);
if(ret < 0)
{
perror("execle error");
}

return 0;
}

在终端执行以下命令生成可执行文件:

1
gcc test.c -Wall # 生成可执行文件 a.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>

extern char **environ;

int main(int argc, char *argv[])
{
char **ep = NULL;
int i = 0;
printf("This is new application!argc = %d\n", argc);
for (i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}

puts("env:");
for (ep = environ; *ep != NULL; ep++)
{
printf(" %s\n", *ep);
}

return 0;
}

在终端执行以下命令生成可执行文件:

1
gcc newTest.c -o newTest -Wall # 生成可执行文件 newTest

在终端执行以下命令运行 a.out :

1
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
6
7
8
9
This is test.c!
This is new application!argc = 3
argv[0]: ./newTest
argv[1]: Hello
argv[2]: fanhua
env:
NAME=app
AGE=25
SEX=man

可以发现, NULL 后边的参数都没有打印出来,且新执行程序的 argv[0] 不再是程序名称,而是与传入的参数保持一致。

3. execv开头

3.1 execv()

3.1.1 函数说明

在 linux 下可以使用 man 3 execv 命令查看该函数的帮助手册。

1
2
3
#include <unistd.h>
extern char **environ; /* 手册上有写,但是似乎不写也没什么 */
int execv(const char *pathname, char *const argv[]);

【函数说明】该函数将新程序加载到某一进程的内存空间,将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main() 函数开始执行。

【函数参数】

  • pathname : const char * 类型,参数 pathname 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
  • argv[] : char *const 类型的数组,指定了传递给新程序的命令行参数。该参数是一个字符串数组,该数组对应于 main(int argc, char *argv[]) 函数的第二个参数 argv ,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。 argv[0] 对应的便是新程序自身路径名。

【返回值】返回值为 int 类型,调用成功将不会返回;失败将返回 -1 ,并设置 errno 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
char *arg[] = {"./newTest", "Hello", "World", NULL, "!"};
execv("./newTest", arg);

【注意事项】 none

3.1.2 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

#include <unistd.h>
int main(int argc, char *argv[])
{
char *arg[] = {"ls", "-a", "-l", "./", NULL};
if(execv("/bin/ls", arg) < 0)
{
perror("execv");
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
总用量 198
drwxrwxrwx 1 root root 4096 5月 23 13:47 .
dr-xr-xr-x 1 root root 4192 5月 23 2022 ..
drwxrwxrwx 1 root root 4096 3月 1 20:41 1Pictures
# 剩下的省略 ...

3.2 execvp()

3.2.1 函数说明

在 linux 下可以使用 man 3 execvp 命令查看该函数的帮助手册。

1
2
3
#include <unistd.h>
extern char **environ; /* 手册上有写,但是似乎不写也没什么 */
int execvp(const char *file, char *const argv[]);

【函数说明】该函数借助 PATH 环境变量将新程序加载到某一进程的内存空间,然后从新程序的 main() 函数开始执行,与 execv 函数功能一样,只是该函数不需要给出新程序的路径。

【函数参数】

  • file : const char * 类型,参数 file 指向需要载入当前进程空间的新程序的名称,不需要给出路径,但是要保证新程序路径存在于环境变量 PATH 中。
  • argv[] : char *const 类型的数组,指定了传递给新程序的命令行参数。该参数是一个字符串数组,该数组对应于 main(int argc, char *argv[]) 函数的第二个参数 argv ,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。 argv[0] 对应的便是新程序自身路径名。

【返回值】返回值为 int 类型,调用成功将不会返回;失败将返回 -1 ,并设置 errno 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
char *arg[] = {"newTest", "Hello", "World", NULL, "!"};
execvp("newTest", arg);

【注意事项】相对于 execv , execvp 仅要求提供新程序的名称即可。

3.2.2 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

#include <unistd.h>
int main(int argc, char *argv[])
{
if(execvp("ls", "ls", "-a", "-l", "./", NULL) < 0)
{
perror("execvp");
}
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
总用量 198
drwxrwxrwx 1 root root 4096 5月 23 13:17 .
dr-xr-xr-x 1 root root 4192 5月 23 2022 ..
drwxrwxrwx 1 root root 4096 3月 1 20:41 1Pictures
# 剩下的省略

3.3 execvpe()

3.3.1 函数说明

在 linux 下可以使用 man 3 execvpe 命令查看该函数的帮助手册。

1
2
3
#include <unistd.h>
extern char **environ; /* 手册上有写,但是似乎不写也没什么 */
int execvpe(const char *file, char *const argv[], char *const envp[]);

【函数说明】该函数将新程序加载到某一进程的内存空间,将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main() 函数开始执行。

【函数参数】

  • file : const char * 类型,参数 file 指向需要载入当前进程空间的新程序的名称,不需要给出路径,但是要保证新程序路径存在于环境变量 PATH 中。
  • argv[] : char *const 类型的数组,指定了传递给新程序的命令行参数。该参数是一个字符串数组,该数组对应于 main(int argc, char *argv[]) 函数的第二个参数 argv ,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。 argv[0] 对应的便是新程序自身路径名。
  • envp[] : char *const 类型数组,即是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value 。

【返回值】返回值为 int 类型,调用成功将不会返回;失败将返回 -1 ,并设置 errno 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
char *arg_arr[5] = {"newTest", "Hello", "World", NULL, };
char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL};
execle("newTest", arg_arr, NULL, env_arr);

【注意事项】

(1)对 execvpe() 的成功调用将永不返回,而且也无需检查它的返回值,一旦该函数返回,就表明它发生了错误。

(2)当使用 execvpe() 传入命令行及环境变量参数时,都是遇到 NULL 就结束,新执行程序的命令行参数的 argv[0] 由调用该新程序的程序传入的第一个参数决定。

3.3.2 使用实例

点击查看实例

首先添加环境变量(不添加的话,可能会报文件找不到。):

1
2
3
4
# 临时改变当前终端的环境变量
export PATH=/home/hk/2Sharedfiles:$PATH
# 查看环境变量是否添加成功
echo $PATH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

#include <unistd.h>
int main(int argc, char *argv[])
{
int ret = 0;
char *arg_arr[5] = {"newTest", "Hello", "qidaink", NULL, "!"};
char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL, "Fanhua=1"};

printf("This is test.c!\n");
ret = execvpe("newTest", arg_arr, env_arr);
if(ret < 0)
{
perror("execvpe error");
}

return 0;
}

在终端执行以下命令生成可执行文件:

1
gcc test.c -Wall # 生成可执行文件 a.out

不过很奇怪的就是,它报了下边的警告:

1
2
3
4
5
6
test.c: In function ‘main’:
test.c:11:8: warning: implicit declaration of function ‘execvpe’; did you mean ‘execvp’? [-Wimplicit-function-declaration]
11 | ret = execvpe("newTest", arg_arr, env_arr);
| ^~~~~~~
| execvp

我查了手册,这个函数是存在的,所以目前还未知原因。后边再补充吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>

extern char **environ;

int main(int argc, char *argv[])
{
char **ep = NULL;
int i = 0;
printf("This is new application!argc = %d\n", argc);
for (i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}

puts("env:");
for (ep = environ; *ep != NULL; ep++)
{
printf(" %s\n", *ep);
}

return 0;
}

在终端执行以下命令生成可执行文件:

1
gcc newTest.c -o newTest -Wall # 生成可执行文件 newTest

在终端执行以下命令运行 a.out :

1
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
6
7
8
9
This is test.c!
This is new application!argc = 3
argv[0]: newTest
argv[1]: Hello
argv[2]: qidaink
env:
NAME=app
AGE=25
SEX=man

可以发现, NULL 后边的参数都没有打印出来,且新执行程序的 argv[0] 不再是程序名称,而是与传入的参数保持一致。

3.4 execve()

3.4.1 函数说明

在 linux 下可以使用 man 3 execve 命令查看该函数的帮助手册。

1
2
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);

【函数说明】该函数将新程序加载到某一进程的内存空间,将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main() 函数开始执行。

【函数参数】

  • pathname : const char * 类型,参数 pathname 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
  • argv[] : char *const 类型的数组,指定了传递给新程序的命令行参数。该参数是一个字符串数组,该数组对应于 main(int argc, char *argv[]) 函数的第二个参数 argv ,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。 argv[0] 对应的便是新程序自身路径名。
  • envp[] : char *const 类型数组,即是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value 。

【返回值】返回值为 int 类型,调用成功将不会返回;失败将返回 -1 ,并设置 errno 。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
/* 需要包含的头文件 */
#include <unistd.h>

/* 至少应该有的语句 */
char *arg_arr[5] = {NULL, "Hello", "World", NULL};
char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL};
execve(argv[1], arg_arr, env_arr);

【注意事项】

(1)对 execve() 的成功调用将永不返回,而且也无需检查它的返回值,一旦该函数返回,就表明它发生了错误。

(2)当使用 execve() 传入命令行及环境变量参数时,都是遇到 NULL 就结束,新执行程序的命令行参数的 argv[0] 由调用该新程序的程序传入的第一个参数决定。

3.4.2 使用实例

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

#include <unistd.h>
int main(int argc, char *argv[])
{
int ret = 0;
char *arg_arr[5] = {"Hello", "world", "!", NULL, "Fanhua"};
char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL, "Fanhua=1"};

printf("This is test.c!\n");
ret = execve("./newTest", arg_arr, env_arr);
if(ret < 0)
{
perror("execve error");
}

return 0;
}

在终端执行以下命令生成可执行文件:

1
gcc test.c -Wall # 生成可执行文件 a.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>

extern char **environ;

int main(int argc, char *argv[])
{
char **ep = NULL;
int i = 0;
printf("This is new application!argc = %d\n", argc);
for (i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}

puts("env:");
for (ep = environ; *ep != NULL; ep++)
{
printf(" %s\n", *ep);
}

return 0;
}

在终端执行以下命令生成可执行文件:

1
gcc newTest.c -o newTest -Wall # 生成可执行文件 newTest

在终端执行以下命令运行 a.out :

1
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
6
7
8
9
This is test.c!
This is new application!argc = 3
argv[0]: Hello
argv[1]: world
argv[2]: !
env:
NAME=app
AGE=25
SEX=man

可以发现, NULL 后边的参数都没有打印出来,且新执行程序的 argv[0] 不再是程序名称,而是与传入的参数保持一致。

二、 system()

1. 函数说明

在 linux 下可以使用 man 3 system 命令查看该函数的帮助手册。

1
2
#include <stdlib.h>
int system(const char *command);

【函数说明】该函数可以在我们当前程序当中执行任意 shell 命令。

点击查看函数详细说明

system() 函数其内部的是通过调用 fork() 、 execl() 以及 waitpid() 这三个函数来实现它的功能。 system() 会调用 fork() 创建一个子进程来运行 shell (可以把这个子进程称为 shell 进程),并通过 shell 执行参数 command 所指定的命令。

【函数参数】

  • command : const char * 类型,参数 command 指向需要执行的 shell 命令,以字符串的形式提供,如 “ls -al” 等。

【返回值】返回值为 int 类型,当参数 command 为 NULL ,如果 shell 可用则返回一个非 0 值,若不可用则返回 0 ;针对一些非 UNIX 系统,该系统上可能是没有 shell 的,这样就会导致 shell 不可能;如果 command 参数不为 NULL ,则返回值从以下的各种情况所决定:

(1)如果无法创建子进程或无法获取子进程的终止状态,那么 system() 返回-1;

(2)如果子进程不能执行 shell ,则 system() 的返回值就好像是子进程通过调用 _exit(127) 终止了;

(3)如果所有的系统调用都成功, system() 函数会返回执行 command 的 shell 进程的终止状态。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <stdlib.h>

/* 至少应该有的语句 */
system("ps -l");

【注意事项】system() 的主要优点在于使用上方便简单,编程时无需自己处理对 fork() 、 exec 函数、 waitpid() 以及 exit() 等调用细节, system() 内部会代为处理;当然这些优点通常是以牺牲效率为代价的,使用 system() 运行 shell 命令需要至少创建两个进程,一个进程用于运行 shell 、另外一个或多个进程则用于运行参数 command 中解析出来的命令,每一个命令都会调用一次 exec 函数来执行;所以从这里可以看出,使用 system() 函数其效率会降低,如果我们的程序对效率或速度有所要求,那么建议不要直接使用 system() 。

2. 函数实现

上边的了解仅仅够我们了解到如何使用这个函数,以及这个函数有什么功能,那他内部是怎样的,使用过程中会有什么影响?我们先来看一下这个函数的源码,很遗憾,我没找到,它在 stdlib.h 文件中声明,但是我还是没有找到源码,就网上搜了一个,应该大差不差:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int system(const char *cmdstring)

{
pid_t pid;
int status;

if (cmdstring == NULL)
{
return (1);
}

if ((pid = fork()) < 0)
{
status = -1;
}

else if (pid == 0)
{

execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
-exit(127); // 子进程正常执行则不会执行此语句
}
else
{
while (waitpid(pid, &status, 0) < 0)
{
if (errno != EINTER)
{
status = -1;
break;
}
}
}
return status;
}

从这里可以看出其实system函数调用之后会创建一个子进程,子进程调用execl()去执行相应的shell命令,然后父进程会调用waitpid等待子进程退出,对子进城进程回收,这也就意味着,调用system的进程在这个时候会被阻塞,当执行的shell命令需要较长时间才能执行完毕的时候,调用system的进程就会等待很久的时间。

3. 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
#include <stdio.h>

#include <stdlib.h>
int main(int argc, char *argv[])
{
system("ps -l");
return 0;
}

在终端执行以下命令:

1
2
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行程序

程序执行后,终端将会显示以下信息:

1
2
3
4
5
F S   UID     PID    PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S 1000 11992 9420 0 80 0 - 5035 do_wai pts/1 00:00:00 bash
0 S 1000 17347 11992 0 80 0 - 595 do_wai pts/1 00:00:00 a.out
0 S 1000 17348 17347 0 80 0 - 658 do_wai pts/1 00:00:00 sh
0 R 1000 17350 17348 0 80 0 - 5254 - pts/1 00:00:00 ps

4. 耗时测试实例

这部分会用到后边线程的一些知识,耗时的测试是通过线程来完成。

4.1 debug_printf.h

点击查看详情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#ifndef __DEBUG_PRINTF_H__
#define __DEBUG_PRINTF_H__

#include <stdio.h>

#define DEBUG_ENABLE 1 /* 是否开启调试信息 */
#define LOG_LEVEL DEBUG_ALL /* 调试信息显示级别 */

#define ERROR "ERROR" /* 错误 */
#define WARN "WARN " /* 警告 */
#define INFO "INFO " /* 信息 */
/* 颜色定义——字体颜色 */
#define CLS_ALL "\033[0m" /* 清除所有颜色 */
#define F_BLACK "\033[30m" /* 黑色字体 */
#define F_RED "\033[31m" /* 红色字体 */
#define F_GREEN "\033[32m" /* 绿色字体 */
#define F_YELLOW "\033[33m" /* 黄色字体 */
#define F_BLUE "\033[34m" /* 蓝色字体 */
#define F_PURPLE "\033[35m" /* 紫色字体 */
#define F_CYAN "\033[36m" /* 青色字体 */
#define F_WHITE "\033[37m" /* 白色字体 */
/* 颜色定义——背景颜色 */
#define B_BLACK "\033[40m" /* 黑色背景 */
#define B_RED "\033[41m" /* 红色背景 */
#define B_GREEN "\033[42m" /* 绿色背景 */
#define B_YELLOW "\033[43m" /* 黄色背景 */
#define B_BLUE "\033[44m" /* 蓝色背景 */
#define B_PURPLE "\033[45m" /* 紫色背景 */
#define B_CYAN "\033[46m" /* 青色背景 */
#define B_WHITE "\033[47m" /* 白色背景 */
/* 颜色定义——背景的字体 */
#define BLACK_RED "\033[30;41m" /* 红底黑字 */
#define BLACK_GREEN "\033[30;42m" /* 绿底黑字 */
#define BLACK_YELLOW "\033[30;43m" /* 黄底黑字 */

/* 调试等级枚举类型定义 */
enum DEBUG_LEVEL
{
DEBUG_OFF = 0,
DEBUG_ERROR = 1,
DEBUG_WARN = 2,
DEBUG_INFO = 3,
DEBUG_ALL = 4
};
//===========================================================================
/* 自定义类型和字体颜色 */
#define MYDEBUG(TYPE, COLOR, FMT, ARGS...) \
do \
{ \
if(DEBUG_ENABLE && (DEBUG_##TYPE <= LOG_LEVEL)) \
{ \
printf(COLOR); \
printf("[%s][%s:%s:%d]"FMT, TYPE, __FILE__, __FUNCTION__, __LINE__, ##ARGS); \
printf(CLS_ALL"\n"); \
} \
} while(0)
//===========================================================================
/* 平时调试使用 */
#include <string.h>
#include <time.h> // 时间函数用
#define filename(x) (strrchr(x,'/')?(strrchr(x,'/')+1):x)
#define NORMAL_DEBUG 1 // 注意使用下边两个宏需要自己添加换行,比较符合平时的习惯
// 获取时间函数
char *timeString(void)
{
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
struct tm *timeinfo = localtime(&ts.tv_sec);
static char timeString[30];
sprintf(timeString, "%.2d-%.2d %.2d:%.2d:%.2d",
timeinfo->tm_mon + 1,
timeinfo->tm_mday,
timeinfo->tm_hour,
timeinfo->tm_min,
timeinfo->tm_sec);
return timeString;
}
// 获取系统时间戳 单位 ms
int sys_pts_get_ms(void)
{
struct timespec tv;
long long lasttime = 0;

clock_gettime(CLOCK_MONOTONIC, &tv);
lasttime = tv.tv_sec * 1000 + tv.tv_nsec / (1000 * 1000);
return lasttime;

}

#define HKPRT(fmt...) \
do \
{ \
if(NORMAL_DEBUG) \
{ \
printf(F_YELLOW); \
printf("[%s][HK_LOG][%s:%d][%s]", timeString(), filename(__FILE__), __LINE__, __FUNCTION__); \
printf(CLS_ALL); \
printf(fmt); \
} \
} while(0)
#define HKPRTE(fmt...) \
do \
{ \
if(NORMAL_DEBUG) \
{ \
printf(F_RED); \
printf("[%s][HK_LOG][%s:%d][%s]", timeString(), filename(__FILE__), __LINE__, __FUNCTION__); \
printf(CLS_ALL); \
printf(fmt); \
} \
} while(0)

#endif

4.2 system_called.c

点击查看详情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <unistd.h>
#include "debug_printf.h"
int main(int argc, char *argv[])
{
/* code */
int time0 = 0;
int time1 = 0;
time0 = sys_pts_get_ms();
HKPRTE("This is system_called!!!\n");
sleep(1);
time1 = sys_pts_get_ms();
HKPRTE("system_called use time:%d\n", time1-time0);
return 0;
}

这个文件我们使用以下命令编译:

1
gcc system_called.c -o system_called -Wall

然后便会生成 system_called 可执行文件,这个文件我们会在测试程序中通过system函数执行这个可执行文件。

4.3 test.c

点击查看详情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/* 头文件 */
#include <stdio.h> /* perror fgets shmat*/
#include <sys/shm.h> /* shmget shmat*/
#include <sys/ipc.h> /* shmget ftok */
#include <string.h> /* strcpy */
#include <stdlib.h> /* system */
#include <sys/prctl.h>
#include <unistd.h>

#include <fcntl.h> /* sem_init For O_* constants */
#include <sys/stat.h> /* sem_init For mode constants */
#include <semaphore.h>/* sem_init sem_post */
#include <pthread.h> /* pthread_create */
#include <signal.h> /* signal */

#include "debug_printf.h"

sem_t sem_sys; /* 定义信号量 */

void *thread(void *arg); /* 读取数据线程 */
void deleteSemfile(int sig);/* 删除无名信号量 */
void sigintHandle(int sig); /* 捕获 Ctrl+\ */


/* 主函数 */
int main(int argc, char *argv[])
{

pthread_t tid;

/* 信号捕获 */
signal(SIGINT, deleteSemfile);
signal(SIGQUIT, sigintHandle);
/* 信号量初始化) */
sem_init(&sem_sys, 0, 0);
/* 创建线程 */
pthread_create(&tid, NULL, thread, NULL);
/* 写入数据 */
while(1)
{
sleep(1);
}

return 0;
}

/* 线程函数 */
void *thread(void *arg)
{
int time0 = 0;
int time1 = 0;
char * main_name = "thread";
prctl(PR_SET_NAME, (unsigned long)main_name);
pthread_detach(pthread_self());
while(1)
{
sem_wait(&sem_sys);
time0 = sys_pts_get_ms();
HKPRT("system call ./system_called\n");
system("./system_called");
time1 = sys_pts_get_ms();
HKPRT("system use time:%d\n", time1-time0);
}
pthread_exit("thread return!");
}
/* 捕获 Ctrl+\ */
void sigintHandle(int sig)
{
static int count = 0;
HKPRT("I catch the SIGQUIT![%d times] \n", ++count);
sem_post(&sem_sys);
}
/* 删除信号量 */
void deleteSemfile(int sig)
{
sem_destroy(&sem_sys);
HKPRT("Destroy sem_sys!\n");
exit(0);
}

这是我们真正的测试函数,我们使用以下命令编译:

1
gcc test.c -lpthread -Wall

这样我们便会生成一个 a.out 可执行文件,a.out可执行文件运行后,我们在终端按下 ctrl + \ 就可以产生一个 SIGQUIT 信号。

4.4 测试结果

我们的上边的几个文件都放在同一目录下,然后我们执行a.out 可执行文件,再按下 ctrl+\ ,会有以下内容输出:

1
2
3
4
5
6
hk@vm:~/6temp/test$ ./a.out 
^\[01-15 15:18:19][HK_LOG][test.c:70][sigintHandle]I catch the SIGQUIT![1 times]
[01-15 15:18:19][HK_LOG][test.c:59][thread]system call ./system_called
[01-15 15:18:19][HK_LOG][system_called.c:10][main]This is system_called!!!
[01-15 15:18:20][HK_LOG][system_called.c:13][main]system_called use time:1001
[01-15 15:18:20][HK_LOG][test.c:62][thread]system use time:1004

可以看到,我们的 a.out 中的 thread 线程执行一次一共耗时1004ms,而 system_called 可执行程序就耗费了1001ms,于是我们知道,system在某个线程中被调用的时候我们是要考虑到system执行的时间的。