Unix 进程 API:fork、wait、exec介绍

本文,主要介绍进程创建的几个接口,带领大家了解进程创建与控制过程。

fork 系统调用

如下,为一个fork调用基本示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv)
{
    int pid = -1;
    pid = getpid();
    printf("hello world (pid:%d)\n", pid);

    int ret = -1;
    ret = fork();
	if (ret < 0) {
      fprintf(stderr, "fork failed\n");
      exit(1);
	} else if (ret == 0) { /* child: new process */
      pid = getpid();
      printf("hello, I am child (pid:%d)\n", pid);
	} else {               /* parent process */
      pid = getpid();
      printf("hello, I am parent of %d (pid:%d)\n", ret, pid);
	}
    
  return 0;
}

当这个程序运行时,首先输出hello world信息,以及自己的进程描述符(PID = 21281)。

紧接着进程掉调用了 fork() 系统调用,这是操作系统提供的创建进程的方法。新创建的进程几乎与调用进程完全一样,对于操作系统来说,这是看起来两个完全一样的程序在运行,并且都从fork系统调用返回。

新创建的进程称为子进程(child)、原来的进程称为父进程(parent),子进程不会从main函数开始执行,而是直接从fork系统调用返回,就好像它自己调用了fork()。父进程获得返回值是创建新进程的PID,而子进程获得返回值是0.

子进程并不是完全拷贝父进程。虽然它拥有自己的地址空间、寄存器、程序寄存器等,但是它从fork()返回值是不同的。

注意:子进程被创建后,它们的输出先后是随机的,这涉及到CPU调度,决定了哪个时刻该运行哪个程序。

wait 系统调用

fork()系统调用,只是创建了一个子进程,其他什么也没做。有时候,我们需要父进程等待子进程执行完毕。这项任务由wait()系统调用(或者更完整的兄弟接口waitpid())完成。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv)
{
    int pid = -1;
    pid = getpid();
    printf("hello world (pid:%d)\n", pid);

    int ret = -1;
    ret = fork();
	if (ret < 0) {
      fprintf(stderr, "fork failed\n");
      exit(1);
	} else if (ret == 0) { /* child: new process */
      pid = getpid();
      printf("hello, I am child (pid:%d)\n", pid);
	} else {               /* parent process */
      int wc = wait(NULL);
      pid = getpid();
      printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", ret,wc,  pid);
	}
    
  return 0;
}

在这个例子中,父进程调用wait(),延迟自己的执行,直到子进程执行完毕。当子进程完结束后,wait()才返回父进程。

exec 系统调用

exec() 系统调用,它也是创建进程API的一个重要组成部分。这个系统调用可以让子进程执行与父进程不同的程序。(exec有几种变体,execl、execle、execlp、execv和execvp)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc, char *argv)
{
    int pid = -1; 
    pid = getpid();
    printf("hello world (pid:%d)\n", pid);

    int ret = -1; 
    ret = fork();
    if (ret < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (ret == 0) { /*child: new process*/
        pid = getpid();
        printf("hello, I am child (pid:%d)\n", pid);                                                                                                                                                        
        char *myargs[3];
        myargs[0] = strdup("wc");  /* program: wc (word count) */
        myargs[1] = strdup("exec.c");/* argumnt: file count*/
        myargs[2] = NULL;             /* marks end of array*/
        execvp(myargs[0], myargs);  /* runs word count */
        printf("this is shouldn't print out");
    } else {               /* parent process */
        int wc = wait(NULL);
        pid = getpid();
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", ret,wc, pid);
    }

    return 0;
}

在这个例子中,子进程调用execvp()来运行字符计数程序wc。实际上,它针对源码文件 exec.c 运行wc,从而告诉我们该文件有多少行,多少单词,以及多少字节。

exec调用,对于给定可执行程序的名称(如wc)以及需要的参数后,就会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化。然后操作系统就执行该程序,将参数通过argv传递给进程。

注:想象一下,可执行程序的加载,是不是很像?

因此,它并没有创建新的进程,而是直接将当前运行的程序(以前的a.out)替换为不同的运行程序(wc)。

子进程执行exec()后,几乎就像exec.c 从未运行过一样。对exec的成功调用永远也不会返回。

为什么要这样设计API?

事实证明,这种分离fork()以及exec()的做法在构建UNIX shell的时候非常有用,因为这给了 shell 在fork 之后 exec 之前运行代码的机会,这些代码可以在运行新程序前改变环境。

shell 也是一个用户程序。它首先是一个显示提示符(prompt),然后等待用户输入。你可以向它输入命令(一个可执行程序的名称及参数),大多数情况下,shell 可以在文件系统中找到这个程序,调用fork()创建新进程,并调用exec()的某个变体来执行这个可执行程序,调用wait()等待该命令的完成。子进程执行结束后,shell 从wait()返回并再次输出提示符,等待用户输入下一个命令。

fork() 和 exec()的分离,让shell 可以方便地实现很多有用的功能。比如:

/* 将输出结果重定向到 newfile.txt*/
wc exec.c > newfile.txt
  • 创建子进程。
  • shell 在调用 exec() 之前,关闭了标准输出,打开了文件 newfile.txt
  • 然后将结果写道 newfile.txt.

如下展示了重定向的基本原理:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <fcntl.h>

int main(int argc, char *argv)
{
    int pid = -1;
    pid = getpid();
    printf("hello world (pid:%d)\n", pid);

    int ret = -1; 
    ret = fork();
    if (ret < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (ret == 0) { /*child: new process*/
        /*pid = getpid();*/
        /*printf("hello, I am child (pid:%d)\n", pid);*/

        close(STDOUT_FILENO);
        open("./redirect.output", O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);

        // now exec wc
        char *myargs[3];
        myargs[0] = strdup("wc");  /* program: wc (word count) */
        myargs[1] = strdup("redirect.c");                   myargs[2] = NULL;
        execvp(myargs[0], myargs);
      } else {               /* parent process */
          int wc = wait(NULL);
          //pid = getpid();
          // printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", ret,wc, pid);
      }
      return 0;
}

UNIX 系统从0 开始寻找可以使用的文件描述符。在这个例子中,STDOUT_FILENO将成为第一个可用的文件描述符。因此在OPEN()被调用时,得到赋值,然后子进程向标准输出文件的写入(例如printf()这样的函数),都会被透明转向新打开的文件,而不少屏幕。

总结

本文只是从较高层面简单介绍了进程API,关于这些系统调用的细节,需要深入学习可了解。另外,除了fork()、exec()、wait()之外,在UNIX中还有其他许多与进程交互的方式。

参考资料

[1].现代操作系统导论.

  • 2
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值