linux c进程的创建、终止

对进程不是很了解的同学可以参考 linux 进程 这篇文章。

进程的创建

系统调用fork() 创建一新进程(子进程)。

#include <unistd.h>

pid_t fork(void);

//失败返回-1

在父进程,fork()会返回子进程的pid,在子进程会返回0。一般由此来区别父子进程。子进程的栈、数据段、堆全部从父进程中复制过来。之后,每个进程都可以修改自己的而不会影响另一进程。

pid_t childPid;

switch (childPid = fork()) {
    case -1:
        //出错控制
    case 0:
        //子进程
    default:
        //父进程
}

fork() 之后到底哪个进程先执行时不确定的。父子进程的文件描述符是复制的,所以它们指向同一打开文件表。取之前文章 linux文件io 的一张图。所以它们共享文件偏移量和状态标志等。

fork() 的内存语义

从概念上讲,可以将fork()认作对父进程程序段、数据段、堆段以及栈段的创建拷贝。不过真要是简单的将父进程的虚拟内存页拷贝到新的子进程,那就太浪费了。原因有很多,其中之一是:fork() 之后常常伴随exec(),这会用新程序替换进程的代码段,并重新初始化数据段、堆段和栈段。那之前对父进程的拷贝就多此一举。为了避免,现代的UNIX实现(包括Linux)会采用两种技术来避免这种浪费。

1.内核将每一进程的代码段标记为只读,这样子进程就无需拷贝父进程的代码段,而直接可以共用代码段。

2.内核采用 写时复制(copy-on-write)技术,调用fork()之后,内核会为一些将要修改的内存页面创建拷贝,还会对子进程的相应页表做适当的调整。这样父、子进程可以分别修改自己的拷贝页,不再相互影响。


进程的终止

通常,进程有两种方式终止。一是异常终止,比如由信号,可能产生core dump文件。另一个则是使用_exit()系统调用正常终止。

#include <unistd.h>

void _exit(int status);

status参数定义了进程的终止状态,虽然为int,但是只有低8位能被父进程所用,0表示正常终止,非0代表出错。

程序一般不会直接调用_exit(),而是调用库函数exit(),它会在调用_exit()前执行各种动作。

1.调用退出处理程序(通过atexit()和on_exit()注册的函数),其执行顺序与注册顺序相反,后面讲解。
2.刷新stdio 流缓冲区。
3.最后使用由status提供的值执行_exit()系统调用。

在main 函数里使用 return n等同于调用exit(n)。

进程终止会关闭打开文件描述符,释放文件锁等会做很多清理动作。

如果进程是管理终端,那么系统会向该终端前台进程组中的每个进程发送 SIGHUP 信号,接着终端会与会话脱离。


注册退出处理程序

#include <stdlib.h>

int atexit(void (*func)(void));

//成功返回0, 失败非0

函数atexit() 将func加到一个函数列表,进程终止时会反过来调用函数列表中的所有函数。一旦有任一退出函数无法返回,那么就不会再调用剩余的处理程序。

void func(void){
/* Perform some actions */
}


atexit() 有2个限制:
1.退出程序无法获知exit()的退出状态。
2.无法给退出处理程序制定参数。

为了摆脱这些限制,glibc提供了一个非标准的函数:on_exit()

#define _BSD_SOURCE /* Or: #define _SVID_SOURCE */

#include <stdlib.h>
int on_exit(void (*func)(int, void *), void *arg);

//Returns 0 on success, or nonzero on error
void func(int status, void *arg){
/* Perform cleanup actions */
}

这2个函数注册的函数位于同一函数列表。如果程序中同时用到了这两种方式,同样是按照相反的顺序执行相应的退出处理程序。

例子

#define _BSD_SOURCE     /* Get on_exit() declaration from <stdlib.h> */
#include <stdlib.h>
#include <string.h>     /* Commonly used string-handling functions */
#include <sys/types.h>  /* Type definitions used by many programs */
#include <stdio.h>      /* Standard I/O functions */
#include <stdlib.h>     /* Prototypes of commonly used library functions,
                           plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h>     /* Prototypes for many system calls */
#include <errno.h>      /* Declares errno and defines error constants */

static void atexitFunc1(void){
    printf("atexit function 1 called\n");
}

static void atexitFunc2(void){
    printf("atexit function 2 called\n");
}

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

int
main(int argc, char *argv[])
{
    if (on_exit(onexitFunc, (void *) 10) != 0)
        perror("on_exit 1");
    if (atexit(atexitFunc1) != 0)
        perror("atexit 1");
    if (atexit(atexitFunc2) != 0)
        perror("atexit 2");
    if (on_exit(onexitFunc, (void *) 20) != 0)
        perror("on_exit 2");

    exit(2);
}
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out 
on_exit function called: status=2, arg=20
atexit function 2 called
atexit function 1 called
on_exit function called: status=2, arg=10


fork()、stdio缓冲区以及_exit()之间的交互

#define _BSD_SOURCE     /* Get on_exit() declaration from <stdlib.h> */
#include <stdlib.h>
#include <string.h>     /* Commonly used string-handling functions */
#include <sys/types.h>  /* Type definitions used by many programs */
#include <stdio.h>      /* Standard I/O functions */
#include <stdlib.h>     /* Prototypes of commonly used library functions,
                           plus EXIT_SUCCESS and EXIT_FAILURE constants */
#include <unistd.h>     /* Prototypes for many system calls */
#include <errno.h>      /* Declares errno and defines error constants */

int
main(int argc, char *argv[])
{
    //存入用户空间的缓冲区
    printf("Hello world\n");
    
    //存入内核空间的缓冲区
    write(STDOUT_FILENO, "freecls\n", 8);

    //创建子进程
    if (fork() == -1)
        perror("fork");

    exit(EXIT_SUCCESS);
}
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]){
    //存入用户空间的缓冲区
    printf("Hello world\n");
    
    //存入内核空间的缓冲区
    write(STDOUT_FILENO, "freecls\n", 8);

    //创建子进程
    if (fork() == -1)
        perror("fork");

    return 0;
}
[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out 
Hello world
freecls

[root@izj6cfw9yi1iqoik31tqbgz c]# ./a.out > a.txt
[root@izj6cfw9yi1iqoik31tqbgz c]# cat a.txt
freecls
Hello world
Hello world

上面当程序输出到终端,会看到预期的效果。但是当重定向到文件,就不一样了。原因是:

printf()会把输出存入用户空间的缓冲区,当标准输出定位到终端时,因为默认为行缓冲,所以带换行符的字符串 "Hello world\n" 会立马刷新缓冲区输出。

而当标准输出重定向到文件时,由于默认为全缓冲(缓冲区满了才会刷新缓冲区输出),所以在fork()调用之前,"Hello world\n"还处在父进程的用户空间的内存缓冲区,随子进程创建而产生了一个副本。父子进程调用exit()时会各自刷新输出,所以会出现2次。而write 会直接进内核缓冲区,fork() 不会复制这一缓冲区。对缓冲区不了解的可以参考 linux 文件io缓冲

顺序的问题也迎刃而解了,这是因为write() 会直接进入内核缓冲区,而printf() 需要等到exit() 刷新缓冲区时才进内核缓冲区。


上一篇: linux c定时器与休眠
下一篇: linux c监控子进程
作者邮箱: 203328517@qq.com