linux c语言 fork() 和 exec 函数的简介和用法

时间:2022-09-22 17:12:19

linux c语言 fork() 和 exec 函数的简介和用法

 

假如我们在编写1个c程序时想调用1个shell脚本或者执行1段 bash shell命令, 应该如何实现呢?

其实在<stdlib.h> 这个头文件中包含了1个调用shell命令或者脚本的函数 system();直接把 shell命令作为参数传入 system函数就可以了, 的确很方便. 关于system 有一段这样的介绍:   system 执行时内部会自动启用fork() 新建1个进程,  效率没有直接使用fork() 和 exec函数高.

那么这篇文章其实就是介绍一下fork() 和 exec函数的用法, 以及如何使用它们来替代system函数.

1. fork() 函数

1.1 fork() 函数的作用

一般来讲, 我们编写1个普通的c程序, 运行这个程序直到程序结束, 系统只会分配1个pid给这个程序, 也就就说, 系统里只会有一条关于这个程序的进程.

但是执行了fork() 这个函数就不同了.

fork 这个英文单词在英文里是"分叉"意思,  fork() 这个函数作用也很符合这个意思.  它的作用是复制当前进程(包括进程在内存里的堆栈数据)为1个新的镜像. 然后这个新的镜像和旧的进程同时执行下去. 相当于本来1个进程, 遇到fork() 函数后就分叉成两个进程同时执行了. 而且这两个进程是互不影响

参考下面这个小程序:

  1. int fork_3(){
  2. printf("it's the main process step 1!!\n\n");
  3. fork();
  4. printf("step2 after fork() !!\n\n");
  5. int i; scanf("%d",&i);   //prevent exiting
  6. return 0;
  7. }

在这个函数里, 共有两条printf语句, 但是执行执行时则打出了3行信息. 如下图:

linux c语言 fork() 和 exec 函数的简介和用法

为什么呢, 因为fork()函数将这个程序分叉了啊,  见下面的图解:

linux c语言 fork() 和 exec 函数的简介和用法

可以见到程序在fork()函数执行时都只有1条主进程, 所以 step 1 会被打印输出1次.

执行 fork()函数后,  程序分叉成为了两个进程, 1个是原来的主进程,  另1个是新的子进程, 它们都会执行fork() 函数后面的代码, 所以 step2 会被 两条进程分别打印输出各一次, 屏幕上就总共3条printf 语句了!

可以见到这个函数最后面我用了 scanf()函数来防止程序退出,  这时查看系统的进程, 就会发现两个相同名字的进程:

如上图, pid 8808 那个就是主进程了, 而 pid  8809那个就是子进程啊, 因为它的parent pid是 8808啊!

需要注意的是, 假如没有做特殊处理, 子进程会一直存在, 即使fork_3()函数被调用完成,  子进程会和主程序一样,返回调用fork_3() 函数的上一级函数继续执行, 直到整个程序退出.

可以看出, 假如fork_3() 被执行2次,  主程序就会分叉两次, 最终变成4个进程, 是不是有点危险. 所以上面所谓的特殊处理很重要啊!

1.2 区别分主程序和子程序.

实际应用中, 单纯让程序分叉意义不大, 我们新增一个子程序, 很可能是为了让子进程单独执行一段代码. 实现与主进程不同的功能.

要实现上面所说的功能, 实际上就是让子进程和主进程执行不同的代码啊.

所以fork() 实际上有返回值, 而且在两条进程中的返回值是不同的, 在主进程里 fork()函数会返回主进程的pid,   而在子进程里会返回0!   所以我们可以根据fork() 的返回值来判断进程到底是哪个进程, 就可以利用if 语句来执行不同的代码了!

如下面这个小程序fork_1():

  1. int fork_1(){
  2. int childpid;
  3. int i;
  4. if (fork() == 0){
  5. //child process
  6. for (i=1; i<=8; i++){
  7. printf("This is child process\n");
  8. }
  9. }else{
  10. //parent process
  11. for(i=1; i<=8; i++){
  12. printf("This is parent process\n");
  13. }
  14. }
  15. printf("step2 after fork() !!\n\n");
  16. }

我对fork() 函数的返回值进行了判断, 如果 返回值是0, 我就让认为它是子进程, 否则是主程序.  那么我就可以让这两条进程输出不同的信息了.

输出信息如下图:

linux c语言 fork() 和 exec 函数的简介和用法

可以见到 子程序和主程序分别输出了8条不同的信息,  但是它们并不是规则交替输出的, 因为它们两条进程是互相平行影响的, 谁的手快就在屏幕上先输出,  每次运行的结果都有可能不同哦.

下面是图解:

linux c语言 fork() 和 exec 函数的简介和用法

由图解知两条进程都对fork()返回值执行判断,  在if 判断语句中分别执行各自的代码.  但是if判断完成后,  还是会回各自执行接下来的代码. 所以 step2 还是输出了2次.

1.4 使用exit() 函数令子进程在if 判断内结束.

参考上面的函数, 虽然使用if 对 fork() 的返回值进行判断,  实现了子进程和 主进程在if判断的范围内执行了不同的代码,  但是就如上面的流程图, 一旦if执行完成, 他们还是会各自执行后面的代码.

通常这不是我们期望的,  我们更多时会希望子进程执行一段特别的代码后就让他结束,  后面的代码让主程序执行就行了.

这个实现起来很简单, 在子程序的if 条件内最后加上exit() 函数就ok了.

将上面的fork_1()函数修改一下, 加上exit语句:

  1. int fork_1(){
  2. int childpid;
  3. int i;
  4. if (fork() == 0){
  5. //child process
  6. for (i=1; i<=8; i++){
  7. printf("This is child process\n");
  8. }
  9. exit(0);
  10. }else{
  11. //parent process
  12. for(i=1; i<=8; i++){
  13. printf("This is parent process\n");
  14. }
  15. }
  16. printf("step2 after fork() !!\n\n");
  17. }

再看看输出:

linux c语言 fork() 和 exec 函数的简介和用法

可以见到, step2只输出1次了,   这是因为子程序在 if条件内结束了啊, 一旦 if 判断成, 就只剩下1个主进程执行下面的代码了, 这正是我们想要的!

注意: exit() 函数在 stdlib.h 头文件内

流程图:

linux c语言 fork() 和 exec 函数的简介和用法

1.4 使用wait() 函数主程序等子程序执行完成(退出)后再执行.

由上面例子得知,  主程序和子程序的执行次序是随机的,  但是实际情况下, 通常我们希望子进程执行后,  才继续执行主进程.

例如对于上面的fork_1()函数, 我想先输出子进程的8个 "This is child process"  然后再输出 8个 主进程"This is parent process", 改如何做?

wait()函数就提供了这个功能,    在if 条件内的  主进程呢部分内 加上wait() 函数, 就可以让主进程执行fork()函数时先hold 住, 等子进程退出后再执行, 通常会配合子进程的exit()函数一同使用.

我将fork_1()函数修改一下, 添加了wait()语句:

  1. int fork_1(){
  2. int childpid;
  3. int i;
  4. if (fork() == 0){
  5. //child process
  6. for (i=1; i<=8; i++){
  7. printf("This is child process\n");
  8. }
  9. exit(0);
  10. }else{
  11. //parent process
  12. wait();
  13. for(i=1; i<=8; i++){
  14. printf("This is parent process\n");
  15. }
  16. }
  17. printf("step2 after fork() !!\n\n");
  18. }

输出:

linux c语言 fork() 和 exec 函数的简介和用法

见到这时的屏幕输出就很有规律了!

其实wait() 函数还有1个功能, 就是可以接收1个 pid_t(在unistd.h内,其实就是Int啦) 指针类型参数,   给这个参数赋上子进程退出前的系统pid值

流程图:

linux c语言 fork() 和 exec 函数的简介和用法

2. exec 函数组

需要注意的是exec并不是1个函数, 其实它只是一组函数的统称, 它包括下面6个函数:

  1. #include <unistd.h>
  2. int execl(const char *path, const char *arg, ...);
  3. int execlp(const char *file, const char *arg, ...);
  4. int execle(const char *path, const char *arg, ..., char *const envp[]);
  5. int execv(const char *path, char *const argv[]);
  6. int execvp(const char *file, char *const argv[]);
  7. int execve(const char *path, char *const argv[], char *const envp[]);

可以见到这6个函数名字不同, 而且他们用于接受的参数也不同.

实际上他们的功能都是差不多的, 因为要用于接受不同的参数所以要用不同的名字区分它们, 毕竟c语言没有函数重载的功能嘛..

但是实际上它们的命名是有规律的:

exec[l or v][p][e]

exec函数里的参数可以分成3个部分,      执行文件部分,     命令参数部分,   环境变量部分.

例如我要执行1个命令   ls -l /home/gateman

执行文件部分就是  "/usr/bin/ls"

命令参赛部分就是 "ls","-l","/home/gateman",NULL              见到是以ls开头 每1个空格都必须分开成2个部分, 而且以NULL结尾的啊.

环境变量部分, 这是1个数组,最后的元素必须是NULL 例如  char * env[] = {"PATH=/home/gateman", "USER=lei", "STATUS=testing", NULL};

好了说下命名规则:

e后续,  参数必须带环境变量部分,   环境变零部分参数会成为执行exec函数期间的环境变量, 比较少用

l 后续,   命令参数部分必须以"," 相隔, 最后1个命令参数必须是NULL

v 后续,   命令参数部分必须是1个以NULL结尾的字符串指针数组的头部指针.         例如char * pstr就是1个字符串的指针, char * pstr[] 就是数组了, 分别指向各个字符串.

p后续,   执行文件部分可以不带路径, exec函数会在$PATH中找

还有1个注意的是, exec函数会取代执行它的进程,  也就是说, 一旦exec函数执行成功, 它就不会返回了, 进程结束.   但是如果exec函数执行失败, 它会返回失败的信息,  而且进程继续执行后面的代码!

通常exec会放在fork() 函数的子进程部分, 来替代子进程执行啦, 执行成功后子程序就会消失,  但是执行失败的话, 必须用exit()函数来让子进程退出!

下面是各个例子:

2.1  execv 函数

  1. int childpid;
  2. int i;
  3. if (fork() == 0){
  4. //child process
  5. char * execv_str[] = {"echo", "executed by execv",NULL};
  6. if (execv("/usr/bin/echo",execv_str) <0 ){
  7. perror("error on exec");
  8. exit(0);
  9. }
  10. }else{
  11. //parent process
  12. wait(&childpid);
  13. printf("execv done\n\n");
  14. }

注意字符串指针数组的定义和赋值

2.2  execvp 函数

  1. if (fork() == 0){
  2. //child process
  3. char * execvp_str[] = {"echo", "executed by execvp",">>", "~/abc.txt",NULL};
  4. if (execvp("echo",execvp_str) <0 ){
  5. perror("error on exec");
  6. exit(0);
  7. }
  8. }else{
  9. //parent process
  10. wait(&childpid);
  11. printf("execvp done\n\n");
  12. }

2.3 execve 函数

  1. if (fork() == 0){
  2. //child process
  3. char * execve_str[] = {"env",NULL};
  4. char * env[] = {"PATH=/tmp", "USER=lei", "STATUS=testing", NULL};
  5. if (execve("/usr/bin/env",execve_str,env) <0 ){
  6. perror("error on exec");
  7. exit(0);
  8. }
  9. }else{
  10. //parent process
  11. wait(&childpid);
  12. printf("execve done\n\n");
  13. }

2.4 execl 函数

  1. if (fork() == 0){
  2. //child process
  3. if (execl("/usr/bin/echo","echo","executed by execl" ,NULL) <0 ){
  4. perror("error on exec");
  5. exit(0);
  6. }
  7. }else{
  8. //parent process
  9. wait(&childpid);
  10. printf("execv done\n\n");
  11. }

2.5 execlp 函数

  1. if (fork() == 0){
  2. //child process
  3. if (execlp("echo","echo","executed by execlp" ,NULL) <0 ){
  4. perror("error on exec");
  5. exit(0);
  6. }
  7. }else{
  8. //parent process
  9. wait(&childpid);
  10. printf("execlp done\n\n");
  11. }

2.6 execle 函数

  1. if (fork() == 0){
  2. //child process
  3. char * env[] = {"PATH=/home/gateman", "USER=lei", "STATUS=testing", NULL};
  4. if (execle("/usr/bin/env","env",NULL,env) <0){
  5. perror("error on exec");
  6. exit(0);
  7. }
  8. }else{
  9. //parent process
  10. wait(&childpid);
  11. printf("execle done\n\n");
  12. }

输出:

linux c语言 fork() 和 exec 函数的简介和用法

3. fork() 和exec 函数与system()函数比较

见到上面execvp函数的输出. 你会发现 exec函数只是系统调用, 它是不支持管线处理的

而system()函数是支持的.   他的内部会自动fork() 1个子进程,但是效率没有fork() 和 exec配合使用好.

但是exec 支持执行脚本.  所以不需要管线处理的命令或者脚本可以利用fork() 和 exec函数来执行.

4. 利用 fwrite() ,fork() 和exec 函数 替代system()函数.

上面讲过了, 虽然exec函数不支持管线, 而且命令参数复杂, 但是它支持执行脚本啊, 所以我们可以使用fwrite将 有管线处理的命令写入1个脚本中, 然后利用exec函数来执行这个脚本.

下面会编写1个base_exec(char *) 函数, 接收1个字符串参数,   然后执行它.

这里只会大概写出这个函数的逻辑步骤:

1. 利用getuid函数获得当前的pid,  然后利用pid获得当前唯一的文件名, 避免因为相同程序同时执行发生冲突!

2.  利用fwrite函数在 /tmp/下面  建立1个上面文件名的脚本文件.     因为/tmp/ 任何用户都可以读写啊

3.  把命令参数写入脚本

4. 利用fork() 和 exec() 执行这个脚本

5. 有需要的话当exec执行完, 记录日志.

下面就是i代码:

头文件:

base_exec.h

  1. #ifndef __BASE_EXEC_H_
  2. #define __BASE_EXEC_H_
  3. int base_exec(char *) ;
  4. #endif /* BASE_EXEC_H_ */

源文件:

base_exec.c

  1. #include "base_exec.h"
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <unistd.h>
  6. #include <time.h>
  7. #define LOGFILE "/home/gateman/logs/c_exec.log"
  8. int base_exec(char * pcmd){
  9. FILE * pf;
  10. pid_t pid = getpid();
  11. char pfilename[20];
  12. sprintf(pfilename, "/tmp/base_exec%d.sh",pid);
  13. pf=fopen(pfilename,"w"); //w is overwrite, a is add
  14. if (NULL == pf){
  15. printf("fail to open the file base_exec.sh!!!\n");
  16. return -1;
  17. }
  18. fwrite("#!/bin/bash\n", 12, 1, pf);
  19. fwrite(pcmd, strlen(pcmd),1, pf);
  20. fwrite("\n", 1,1, pf);
  21. fclose(pf);
  22. if (fork() ==0 ){
  23. //child processj
  24. char * execv_str[] = {"bash", pfilename, NULL};
  25. if (execv("/bin/bash",execv_str) < 0){
  26. perror("fail to execv");
  27. exit(-1);
  28. }
  29. }else{
  30. //current process
  31. wait();
  32. pf=fopen(LOGFILE,"a");
  33. if (NULL == pf){
  34. printf("fail to open the logfile !!!\n");
  35. return -1;
  36. }
  37. time_t t;
  38. struct tm * ptm;
  39. time(&t);
  40. ptm  = gmtime(&t);
  41. char cstr[24];
  42. sprintf (cstr, "time: %4d-%02d-%02d %02d:%02d:%02d\n", 1900+ptm->tm_year,ptm->tm_mon,ptm->tm_mday,ptm->tm_hour,ptm->tm_min,ptm->tm_sec);
  43. fwrite(cstr, strlen(cstr),1, pf);
  44. int uid = getuid();
  45. sprintf(cstr, "uid: %d\ncommand:\n",uid);
  46. fwrite(cstr, strlen(cstr),1, pf);
  47. fwrite(pcmd, strlen(pcmd),1, pf);
  48. fwrite("\n\n\n", 3,1, pf);
  49. fclose(pf);
  50. remove(pfilename);
  51. return 0;
  52. }
  53. return 0;
  54. }

linux c语言 fork() 和 exec 函数的简介和用法的更多相关文章

  1. linux进程之fork 和 exec函数

    ---恢复内容开始--- fork函数 该函数是unix中派生新进程的唯一方法. #include <unistd.h> pid_t   fork(void); 返回: (调用它一次, 它 ...

  2. Linux下多进程编程之exec函数语法及使用实例

    exec函数族 1)exec函数族说明 fork()函数用于创建一个子进程,该子进程几乎复制了父进程的全部内容,但是,这个新创建的进程如何执行呢?exec函数族就提供了一个在进程中启动另一个程序执行的 ...

  3. fork和exec函数

    #include<unistd.h> pid_t fork(void); 返回:在子进程中为0,在父进程中为子进程IO,若出错则为- fork最困难之处在于调用它一次,它却返回两次.它在调 ...

  4. 软件素材---linux C语言:拼接字符串函数 strcat的用例&lpar;与char数组联合使用挺好&rpar;

    [头文件]#include <string.h> [原型] 1 char *strcat(char *dest, const char *src); [参数]: dest 为目标字符串指针 ...

  5. Linux C语言操作MySQL

    原文:Linux C语言操作MySQL 1.MySQL数据库简介 MySQL是一个开源码的小型关系数据库管理系统,体积小,速度快,总体成本低,开源.MySQL有以下特性: (1) 使用C和C++编写, ...

  6. 从0开始自己用C语言写个shell&lowbar;&lowbar;01&lowbar;整体的框架以及fork和exec族函数的理解

    最近才忙完了一个操作系统的作业,让我们用C语言实现一个Shell.总的来说,其实就是让我们 对系统调用有比较深的了解. 首先 介绍一下我的Shell 所实现的功能.1.运行可执行程序 即输入某个 标志 ...

  7. Linux 环境下 fork 函数和 exec 函数族的使用

    前言 接触 Linux 已经有几个月了,以前在网上看各路大神均表示 Windows 是最烂的开发平台,我总是不以为然,但是经过这段时间琢磨,确实觉得 Linux 开发给我带来不少的便利.下面总结一下学 ...

  8. 【转】Linux下Fork与Exec使用

    Linux下Fork与Exec使用 转自 Linux下Fork与Exec使用 一.引言 对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一:它执行一次却返回两个值.for ...

  9. Linux下Fork与Exec使用

    Linux下Fork与Exec使用   一.引言 对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一:它执行一次却返回两个值.fork函数是Unix系统最杰出的成就之一, ...

随机推荐

  1. NRF52832学习笔记

    一.打印函数 printf("");用于在调试串口时在电脑端的串口调试工具上打印: 宏定义时每行后面的斜杠,在最后一行不加斜杠.

  2. JAVA NIO是什么&lpar;zz&rpar;

    JAVA NIO是什么? 1.   基本 概念 IO 是主存和外部设备 ( 硬盘.终端和网络等 ) 拷贝数据的过程. IO 是操作系统的底层功能实现,底层通过 I/O 指令进行完成. 所有语言运行时系 ...

  3. 网页中的超链接&lt&semi;a&gt&semi;标签

    格式: <a href="目标网址" title="鼠标滑过显示的文本">链接显示的文本</a> 注意:为文本加入<a>标签 ...

  4. Warning&colon; Using innodb&lowbar;additional&lowbar;mem&lowbar;pool&lowbar;size is DEPRECATED

    Warning: Using innodb_additional_mem_pool_size is DEPRECATED. This option may be removed in future r ...

  5. SpringDataMongoDB介绍(一)-入门

    SpringDataMongoDB介绍(一)-入门 本文介绍如何应用SpringDataMongoDB操作实体和数据库,本文只介绍最基本的例子,复杂的例子在后面的文章中介绍. SpringDataMo ...

  6. Decode Ways -- LeetCode

    原题链接: http://oj.leetcode.com/problems/decode-ways/  这道题要求解一个数字串依照字符串编码方式可解析方式的数量.看到这样的求数量的,我们非常easy想 ...

  7. codeforces C&period; No to Palindromes&excl;

    http://codeforces.com/contest/465/problem/C 题意:给你一个字符串,然后按照字典序找出下一个字符串,这个字符串中不能含有长度大于等于2的子串为回文串,如果含有 ...

  8. 18&period;如何自我Struts2它Struts2标签和综合汇总文章有点早

    18.如何自我Struts2它Struts2标签和综合汇总文章有点早[视频] 之前写了一篇"打算做一个视频教程探讨怎样自学计算机相关的技术",优酷上传不了.仅仅好传到百度云上: h ...

  9. IOS学习之路(代码实现自动布局)

    1.将一个试图放置在其父视图的*位置,使用限制条件. 2.创建两个限制条件:一个是将目标视图的 center.x 位置排列在其父视图的 center.x 位置,并且另外一个是将目标视图的 cente ...

  10. API得到Windows版本

    API得到Windows版本 /** * Windows Version * https://msdn.microsoft.com/en-us/library/windows/desktop/dn48 ...