Linux进程的创建结束类系统调用总结

 
Category: Linux-Shell

写在前面

总结一下Linux系统的进程创建/终止/等待等系统调用, 参考:

  1. Linux/Unix系统编程手册.

下面主要给出例子, 关于函数原型可以参考书中或者man 2 syscall(例如man 2 fork).

测试环境: Ubuntu 20.04 x86_64

gcc-9

进程内存布局

进程创建: fork()

用于创建新的进程, 创建出来的新进程称为子进程, 拥有和父进程一样的代码段/数据段/栈段/堆段.

所以创建新进程的资源消耗较大, 后续采用多线程方式可以解决这个问题.

由于这个函数的设计比较奇怪, 有两个返回值, 在父进程中返回子进程的进程ID, 在子进程中返回0, 错误返回-1, 所以可以用下面的语句制定创建子进程之后的进一步操作:

pid_t childPid;

switch (childPid = fork()) {
    case -1:
        /* error handling */
    case 0: // child process
        /* actions to child */
    default:
        /* actions to parent */
}

下面主要讨论数据共享和文件(句柄)共享, 为探讨进程间通信做准备.

数据共享

一个例子, 关于同时操作一份数据:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> // pid_t
#include <unistd.h>    // fork
static int idata = 111;

int main(int argc, char *argv[]) {
    pid_t childPId;
    int istack = 222;

    switch (childPId = fork()) {
        case -1:
            fprintf(stderr, "fork error\n");
        case 0:
            idata *= 3;
            istack *= 3;
            break;
        default:
            sleep(3);
            break;
    }

    printf("PID=%ld %s idata=%d istack=%d\n", (long)getpid(),
           (childPId == 0) ? "(child) " : "(parent)", idata, istack);
    /* PID=526436 (child)  idata=333 istack=666 */
    /* PID=526435 (parent) idata=111 istack=222 */

    return 0;
}

文件共享

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

int main(int argc, char* argv[]) {
    int fd, flags;
    char template[] = "/tmp/test-XXXXXX";
    setbuf(stdout, NULL);// 无缓冲

    fd = mkstemp(template);
    if (fd == -1) fprintf(stderr, "mkstemp");

    printf("File offset before fork: %lld\n",
           (long long)lseek(fd, 0, SEEK_CUR));

    flags = fcntl(fd, F_GETFL);

    if (flags == -1) fprintf(stderr, "fcntl - F_GETFL");
    printf("O_APPEND flag before fork() is %s\n",
           (flags & O_APPEND) ? "on" : "off");

    switch (fork()) {
        case -1:
            fprintf(stderr, "fork");
        case 0: // child
            if (lseek(fd, 1000, SEEK_SET) == -1) fprintf(stderr, "lseek");
            flags = fcntl(fd, F_GETFL);
            if (flags == -1) fprintf(stderr, "fcntl - F_GETFL");
            flags |= O_APPEND;
            if (fcntl(fd, F_SETFL, flags) == -1)
                fprintf(stderr, "fcntl - F_SETFL");
            _exit(EXIT_SUCCESS);
        default: // parent
            if (wait(NULL) == -1) fprintf(stderr, "wait");
            printf("child has exited\n");

            printf("file offset in parent is %lld\n",
                   (long long)lseek(fd, 0, SEEK_CUR));

            flags = fcntl(fd, F_GETFL);
            if (flags == -1) fprintf(stderr, "fcntl - F_GETFL");

            printf("O_APPEND in parent is %s\n",
                   (flags & O_APPEND) ? "on" : "off");
            exit(EXIT_SUCCESS);
    }
    return 0;
}
/* :!cc fork-file-shared.c && ./a.out */
/* File offset before fork: 0 */
/* O_APPEND flag before fork() is off */
/* child has exited */
/* file offset in parent is 1000 */
/* O_APPEND in parent is on */

新进程创建-节约资源版: vfork()

最好不要用vfork

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

int main(int argc, char *argv[]) {
    int istack = 222;
    switch (vfork()) {
        case -1:
            fprintf(stderr, "vfork");
            exit(1);
        case 0: // child
            sleep(3);
            write(STDOUT_FILENO, "child executing\n", 16);
            istack *= 3;
            _exit(EXIT_SUCCESS);
        default:
            write(STDOUT_FILENO, "Parent executing\n", 17);
            printf("istack=%d\n", istack);
            exit(EXIT_SUCCESS);
    }
    return 0;
}
// 子进程对变量的修改影响了父进程的对应变量

/* child executing */
/* Parent executing */
/* istack=666 */

子进程会共享父进程的内存, 父进程会一直挂起直到子进程终止或者调用exec

fork竞态条件与同步

竞态条件

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

int main(int argc, char *argv[]) {
    int numChild, j;
    pid_t childPid;

    numChild = 100;
    setbuf(stdout, NULL); // 关闭缓存
    for (j = 0; j < numChild; ++j) {
        switch (childPid = fork()) {
            case -1:
                fprintf(stderr, "fork\n");
            case 0: // child
                printf("%d child\n", j);
                _exit(EXIT_SUCCESS);
            default: // parent
                printf("%d parent\n", j);
                wait(NULL);
                break;
        }
    }
    return 0;
}

几乎全是父进程先输出结果, 然后是子进程. 这就说明在Linux中fork执行之后会继续执行父进程, 而不是子进程.

不过, 这也取决于内核的调度算法实现.

所以不要对fork之后父子进程的执行顺序做任何假设, 如果一定要确保某一特定的执行顺序, 一定要采用某种进程间通信技术(同步技术), 例如: 文件锁, 信号量, 消息传送(基于管道, pipe).

使用信号机制解决进程竞态条件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <time.h>
#include <sys/types.h>

#define BUF_SIZE 1000
#define SYNC_SIG SIGUSR1 /* Synchronization signal */

static void /* Signal handler - does nothing but return */
handler(int sig) {}

char *currTime(const char *format) {
    static char buf[BUF_SIZE]; /* Nonreentrant */
    time_t t;
    size_t s;
    struct tm *tm;

    t = time(NULL);
    tm = localtime(&t);
    if (tm == NULL) return NULL;

    s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);

    return (s == 0) ? NULL : buf;
}

int main(int argc, char *argv[]) {
    pid_t childPid;
    sigset_t blockMask, origMask, emptyMask;
    struct sigaction sa;

    setbuf(stdout, NULL); /* Disable buffering of stdout */

    sigemptyset(&blockMask);
    sigaddset(&blockMask, SYNC_SIG); /* Block signal */
    if (sigprocmask(SIG_BLOCK, &blockMask, &origMask) == -1)
        fprintf(stderr, "sigprocmask");

    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sa.sa_handler = handler;
    if (sigaction(SYNC_SIG, &sa, NULL) == -1) fprintf(stderr, "sigaction");

    switch (childPid = fork()) {
        case -1:
            fprintf(stderr, "fork");

        case 0: /* Child */

            /* Child does some required action here... */

            printf("[%s %ld] Child started - doing some work\n", currTime("%T"),
                   (long)getpid());
            sleep(2); /* Simulate time spent doing some work */

            /* And then signals parent that it's done */

            printf("[%s %ld] Child about to signal parent\n", currTime("%T"),
                   (long)getpid());
            if (kill(getppid(), SYNC_SIG) == -1) fprintf(stderr, "kill");

            /* Now child can do other things... */

            _exit(EXIT_SUCCESS);

        default: /* Parent */

            /* Parent may do some work here, and then waits for child to
               complete the required action */

            printf("[%s %ld] Parent about to wait for signal\n", currTime("%T"),
                   (long)getpid());
            sigemptyset(&emptyMask);
            if (sigsuspend(&emptyMask) == -1 && errno != EINTR)
                fprintf(stderr, "sigsuspend");
            printf("[%s %ld] Parent got signal\n", currTime("%T"),
                   (long)getpid());

            /* If required, return signal mask to its original state */

            if (sigprocmask(SIG_SETMASK, &origMask, NULL) == -1)
                fprintf(stderr, "sigprocmask");

            exit(EXIT_SUCCESS);
    }
}

/* :!cc fork-sig-sync.c -Wall && ./a.out */
/* [20:01:11 742971] Parent about to wait for signal */
/* [20:01:11 742977] Child started - doing some work */
/* [20:01:13 742977] Child about to signal parent */
/* [20:01:13 742971] Parent got signal */

进程终止

exit和_exit

#include <unistd.h>

void _exit(int status); // syscall

status参数就是传入的终止状态.

#include <stdlib.h>

void exit(int status); // libc

会依次执行下面三个步骤:

  1. 调用退出处理程序: 通过atexit()on_exit()注册的函数
  2. 刷新stdio流缓冲区
  3. 使用由status提供的值执行_exit()系统调用

执行return n;相当于执行exit(n);

进程终止后的细节

  1. 关闭所有打开的文件描述符, 目录流, 信息目录描述符以及转换描述符
  2. 释放进程持有的所有文件锁
  3. 分离任何已连接的System V共享内存段
  4. ...

注册退出处理程序

#include <stdlib.h>

int atexit(void (*func)(void)); // 出错返回非零值, 不一定为-1

其中传入的参数为一个参数列表和返回值均为void的函数指针.

atexit函数会将传入的func函数指针加入到一个函数列表中, 进程终止时会调用该函数列表的所有函数.

以及一个类似的库函数: (可以传入状态)

#include<stdlib.h>

int on_exit(void (*func)(int, void *), void *arg);

其中传入的参数为一个参数列表为int和指针, 返回值为void的函数指针.

例子

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

static void atexitFunc1(void) { printf("atexit func1 called\n"); }
static void atexitFunc2(void) { printf("atexit func2 called\n"); }

static void onexitFunc(int exitStatus, void *arg) {
    printf("on_exit func called: status=%d, arg=%ld\n", exitStatus, (long)arg);
}

int main(int argc, char *argv[]) {

    if (on_exit(onexitFunc, (void *)10)) fprintf(stderr, "on_exit 1\n");
    if (atexit(atexitFunc1)) fprintf(stderr, "atexit 1\n");
    if (atexit(atexitFunc2)) fprintf(stderr, "atexit 2\n");
    if (on_exit(onexitFunc, (void *)20)) fprintf(stderr, "on_exit 2\n");
    return 0;
}

/* on_exit func called: status=0, arg=20 */
/* atexit func2 called */
/* atexit func1 called */
/* on_exit func called: status=0, arg=10 */

监控子进程

wait(): 等待子进程的基本调用

等待调用进程的任一子进程终止, 同时在status所指向的缓冲区中返回子进程的终止状态.

#include<sys/wait.h>

pid_t wait(int *status);

wait()执行如下动作:

  1. 如果调用进程(指父进程)的一个(先前未等待的)子进程已经终止, 调用将一直阻塞, 直至某个子进程终止. 如果调用时已有子进程终止, wait将立即返回
  2. status非空, 则存入状态信息.
  3. 内核将为父进程下所有子进程的运行总量追加进程CPU时间以及资源使用数据.
  4. 将终止子进程的ID作为结果返回.

出错情况: (返回值为-1)

调用进程没有(先前未等待的)子进程, 此时errno置为ECHILD. 所以可以用下面代码检测所有子进程是否退出.

  while (childPid = wait(NULL) != -1) continue;
  if (errno != ECHILD) exit(1); // error

例子

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
#define BUF_SIZE 1000
#define NUM 4
int times[NUM] = {0, 7, 1, 4};

char *currTime(const char *format) {
    static char buf[BUF_SIZE]; /* Nonreentrant */
    time_t t;
    size_t s;
    struct tm *tm;

    t = time(NULL);
    tm = localtime(&t);
    if (tm == NULL) return NULL;

    s = strftime(buf, BUF_SIZE, (format != NULL) ? format : "%c", tm);

    return (s == 0) ? NULL : buf;
}

int main(int argc, char *argv[]) {
    int numDead;
    pid_t childPid;
    int j;
    setbuf(stdout, NULL);
    for (j = 1; j < NUM; ++j) {
        switch (fork()) {
            case -1:
                fprintf(stderr, "fork\n");
            case 0:
                printf(
                    "[%s] child %d started with PID  %ld, sleeping %d "
                    "seconds\n",
                    currTime("%T"), j, (long)getpid(), times[j]);
                sleep(times[j]);
                _exit(0);
            default:
                break;
        }
    }
    numDead = 0;
    for (;;) {
        childPid = wait(NULL);
        if (childPid == -1) {
            if (errno == ECHILD) {
                printf("No more children -bye!\n");
                exit(0);
            } else {
                fprintf(stderr, "wait\n");
            }
        }
        numDead++;
        printf("[%s] wait() return child PID %ld (numDead=%d)\n",
               currTime("%T"), (long)childPid, numDead);
    }
}
/* [08:09:53] child 2 started with PID  825604, sleeping 1 seconds */
/* [08:09:53] child 1 started with PID  825603, sleeping 7 seconds */
/* [08:09:53] child 3 started with PID  825605, sleeping 4 seconds */
/* [08:09:54] wait() return child PID 825604 (numDead=1) */
/* [08:09:57] wait() return child PID 825605 (numDead=2) */
/* [08:10:00] wait() return child PID 825603 (numDead=3) */

waitpid(): 升级版的wait

wait存在以下的一些限制:

  1. 如果父进程已经创建了多个子进程, 使用wait将无法等待某一个确定的 子进程完成, 只能按顺序等待下一个子进程终止.
  2. 如果没有子进程退出, wait总是保持阻塞, 但是有时候会希望执行非阻塞等待.
  3. 使用wait只能发现那些已终止的子进程, 对于子进程因为哪个信号终止或者已停止的子进程收到SIGCONT信号后恢复执行的情况不得而知.
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

其中, 返回值和status参数的描述与wait相同, 下面是参数pid的含义:

  1. $pid>0$: 等待进程的PID为pid的子进程.
  2. $pid=0$: 等待与调用进程(父进程)同一个进程组的所有子进程.
  3. $pid<-1$: 等待进程组标识符与pid绝对值相等的所有子进程.
  4. $pid=-1$: 等待任意子进程. wait(&status);等价于waitpid(-1, &status, 0);.

参数options是一个位掩码, 可以包含(按位或操作)0个或多个如下标志:

  • WUNTRACED: 除了返回终止子进程的信息外, 还返回因信号而停止的子进程信息
  • WCONTINUED: 返回因收到SIGCONT信号而恢复执行的已停止子进程的状态信息
  • WNOHANG:
    • pid指定的子进程状态未改变, 返回, 不阻塞(poll,轮询), waitpid返回0.
    • 调用进程没有与pid匹配的子进程, 报错, errno=ECHILD.

程序的执行

execve: 将新程序加载到某一进程的内存空间

在这一过程中, 将丢弃旧有程序, 而进程的栈,数据以及堆内存都会被新程序的相应部件所替换.

在执行了各种C语言函数库的运行时启动代码以及程序的初始化代码之后, 新程序会从main()函数位置开始执行.

#include <unistd.h>

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

例子:

// envargs.c
#include <stdio.h>

extern char **environ;

int main(int argc, char *argv[]) {
    int j;
    char **ep;
    for (j = 0; j < argc; ++j) printf("argv[%d]=%s\n", j, argv[j]);
    for (ep = environ; *ep != NULL; ++ep) printf("environ: %s\n", *ep);
    return 0;
}

// execve-1.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    char *argVec[10];
    char *envVec[] = {"a=b", "c=d", NULL};
    argVec[0] = strrchr(argv[1], '/');
    if (argVec[0] != NULL)
        argVec[0]++;
    else
        argVec[0] = argv[1];
    argVec[1] = "hello";
    argVec[2] = "goodbye";

    argVec[3] = NULL;

    execve(argv[1], argVec, envVec);
    fprintf(stderr, "execve\n");
    return 0;
}

system: 执行系统命令

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

int main (int argc, char *argv[])
{
    system("ls | wc");
     /* 13      13     170 */
    return 0;
}