进程与子进程

目录

  进程概念

孤儿进程

僵尸进程

1、fork与pid

验证

2、 父进程和子进程文件共享

 验证​

3、fork()后的竞争

验证

 代码如下

4、监视子进程

代码编写之wait

验证

代码编写之waitpid


  进程概念

        当一个进程创建子进程之后,它们俩就成为父子进程关系,父进程与子进程的生命周期往往是不相同的,这里就会出现两个问题:

⚫ 父进程先于子进程结束
⚫ 子进程先于父进程结束

孤儿进程

        父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿进程。在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程, 换言之,某一子进程的父进程结束后,该子进程调用 getppid()将返回 1, init 进程变成了孤儿进程的“养父”

僵尸进程

        进程先于父进程结束,进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用wait()(或其变体 waitpid()、 waitid()等)函数回收子进程资源,归还给系统。

        如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个僵尸进程。 子进程结束后其父进程并没有来得及立马给它“收尸”, 子进程处于“曝尸荒野”的状态,在这么一个状态下,我们就将子进程成为僵尸进程
        父进程通过调用wait()(或其变体 waitpid()、 waitid()等)函数为子进程“收尸”后,僵尸进程就会被内核彻底删除。另外一种情况,如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并自动调用 wait(), 故而从系统中移除僵尸进程
        不“收尸”这样的程序设计是有问题的,如果系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉

1、fork与pid

        fork()调用成功后,将会在父进程中返回子进程的 PID,而在子进程中返回值是 0;如果调用失败,父进程返回值-1,不创建子进程,并设置 errno。
        fork()调用成功后,子进程和父进程会继续执行 fork()调用之后的指令,子进程、父进程各自在自己的进程空间中运行。事实上,子进程是父进程的一个副本, 譬如子进程拷贝了父进程的数据段、堆、栈以及继承了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间,这是子进程对父进程相应部分存储空间的完全复制,执行 fork()之后,每个进程均可修改各自的栈数据以及堆段中的变量,而并不影响另一个进程。
        虽然子进程是父进程的一个副本,但是对于程序代码段(文本段)来说, 两个进程执行相同的代码段,因为代码段是只读的, 也就是父子进程共享代码段,在内存中只存在一份代码段数据

进程与子进程_第1张图片

 利用fork创建子进程,分别使用父进程和子进程打印信息

验证

代码如下 

#include 
#include 
#include 
#include 
void main()
{
    pid_t pid;

    pid = fork();
    /*此时有两个进程在同时运行*/
    if (pid < 0)
    {
        printf("error fork\n");
        exit(-1);
    }
    else if (pid == 0)
    {
        printf("child process id:%d,father id:%d\n", getpid(),getppid());
        _exit(0);/*子进程退出*/
    }
    else
    {
        printf("father process id:%d, child id:%d\n", getpid(),pid);
        exit(0);
    }
}

2、 父进程和子进程文件共享

        调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,这也意味着父、子进程对应的文件描述符均指向相同的文件表,因而这些文件在父、子进程间实现了共享,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置偏移量。

        下面父进程打开文件之后,然后 fork()创建子进程,此时子进程继承了父进程打开的文件描述符(父进程文件描述符的副本) ,然后父、子进程同时对文件进行写入操作

进程与子进程_第2张图片

        父进程 open 打开文件之后,才调用 fork()创建了子进程, 此种情况下,父、子进程分别对同一个文件进行写入操作,结果是接续写,不管是父进程,还是子进程,在每次写入时都是从文件的末尾写入

 验证

代码如下 

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    pid_t pid;
    int fd, i;
    if ((fd = open("./test.txt", O_RDWR | O_CREAT | O_TRUNC, S_IRWXU)) < 0)
    {
        perror("fork error");
        exit(-1);
    }
    pid = fork();
    switch (pid)
    {
    case -1:
        perror("fork error");
        close(fd);
        exit(-1);
    case 0: /*子进程*/
        for (i = 0; i < 4; i++)
            write(fd, "12", 2);
        close(fd);
        _exit(0);
    default: /*父进程*/
        for (i = 0; i < 4; i++)
            write(fd, "AB", 2);
        close(fd);
        exit(0);
    }
}

        再来测试另外一种情况,父进程在调用 fork()之后,此时父进程和子进程都去打开同一个文件,然后再对文件进行写入操作

进程与子进程_第3张图片

         在父子进程中分别打开并写入文件,查看结果,如下

         这种文件共享方式实现的是一种两个进程分别各自对文件进行写入操作,因为父、子进程的这两个文件描述符分别指向的是不同的文件表,意味着它们有各自的文件偏移量,一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况。

3、fork()后的竞争

        调用 fork()之后,子进程成为了一个独立的进程,可被系统调度运行,而父进程也继续被系统调度运行,这里出现了一个问题,调用 fork 之后,无法确定父、子两个进程谁将率先访问 CPU,也就是说无法确认谁先被系统调用运行。

        对于有些特定的应用程序,它对于执行的顺序有一定要求的,譬如它必须要求父进程先
运行,或者必须要求子进程先运行, 程序产生正确的结果它依赖于特定的执行顺序,那么将可能因竞争条件而导致失败、无法得到正确的结果。这个时候可以通过采用采用某种同步技术来实现,譬如前面给介绍的信号,如果要让子进程先运行,则可使父进程被阻塞,等到子进程来唤醒它

进程与子进程_第4张图片

进程与子进程_第5张图片

         子进程先运行打印相应信息,之后再执行父进程打印信息,在父进程分支中,直接调用了 sigsuspend()使父进程进入挂起状态,由子进程通过 kill 命令发送信号唤醒。

        子进程发送的唤醒父进程的信号会等到子进程退出之后才会发送给父进程。因为在子进程调用 kill(getppid(), SIGUSR1) 发送信号之后,它并没有保持活跃状态等待父进程接收信号,而是立即退出了,这时候内核会接管并且在合适的时候向父进程发送信号。

        所以,当父进程在调用 sigsuspend(&wait_mask) 之后被阻塞时,它会等待接收子进程发送的信号,但是此时子进程还没有退出,因此父进程会一直处于阻塞状态,直到收到子进程发送的信号并且被成功唤醒为止。

验证

进程与子进程_第6张图片

 代码如下

#include 
#include 
#include 
#include 
#include 

static void sig_handler(int sig)
{
    printf("接收到信号\n");
}
int main()
{
    struct sigaction sig = {0};
    sigset_t wait_mask;
    sigemptyset(&wait_mask);
    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (sigaction(SIGUSR1, &sig, NULL) == -1)
    {
        perror("sigaction error");
        exit(-1);
    }
    switch (fork())
    {
    case 0:
        /* 子进程 */
        printf("子进程开始执行\n");
        printf("子进程打印信息\n");
        printf("~~~~~~~~~~~~~~~\n");
        sleep(2);
        kill(getppid(), SIGUSR1); // 发送信号给父进程、唤醒它
        _exit(0);
    default:
        /* 父进程 */
        if (-1 != sigsuspend(&wait_mask)) // 挂起、阻塞
            exit(-1);
        printf("父进程开始执行\n");
        printf("父进程打印信息\n");
        exit(0);
    }
}

4、监视子进程

        对于许多需要创建子进程的进程来说,有时设计需要监视子进程的终止时间以及终止时的一些状态信息,在某些设计需求下这是很有必要的。 系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息。

代码编写之wait

#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    int status, ret, i;
    for (i = 0; i < 3; i++)
    {
        switch (fork())
        {
        case -1:
            perror("fork error");
            exit(-1);
        case 0:
            /* 子进程 */
            printf("子进程<%d>被创建\n", getpid());
            sleep(i);
            _exit(i);
        default:
            /* 父进程 */
            break;
        }
    }
    sleep(i);
    printf("~~~~~~~~~~~~~~\n");
    for (i = 1; i <= 3; i++)
    {
        ret = wait(&status);
        if (ret == -1)
        {
            if (ECHILD == errno)/*表示当前没有需要等待回收的子进程*/
            {
                printf("没有需要等待回收的子进程\n");
                exit(0);
            }
            else
            {
                perror("wait error");
                exit(-1);
            }
        }
        printf("回收子进程<%d>, 终止状态<%d>\n", ret, WEXITSTATUS(status));
    }
    exit(0);
}

         父进程执行for循环,共创建了3个子进程,每个子进程先输出自己的pid,然后睡眠一段时间。睡眠结束后,子进程调用_exit()函数退出,并返回一个不同的退出码。这里的退出码是子进程睡眠时间.父进程根据wait()函数返回值判断是否有子进程退出。如果没有需要回收的子进程,则退出循环并正常结束程序。

验证

进程与子进程_第7张图片

使用 wait()系统调用存在着一些限制,这些限制包括如下:
⚫ 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
⚫ 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
⚫ 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。

waitpid()则可以突破这些限制

代码编写之waitpid

#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int status, ret, i;
    /* 循环创建 3 个子进程 */
    for (i = 1; i <= 3; i++)
    {
        switch (fork())
        {
        case -1:
            perror("fork error");
            exit(-1);
        case 0:
            /* 子进程 */
            printf("子进程<%d>被创建\n", getpid());
            sleep(i);
            _exit(i);
        default:
            /* 父进程 */
            break;
        }
    }
    sleep(1);
    printf("~~~~~~~~~~~~~~\n");
    for (;;)
    {
        ret = waitpid(-1, &status, WNOHANG);/*执行非阻塞等待,可以实现轮询*/
        if (ret <0)
        {
            if (ECHILD == errno)
                exit(0);
            else
            {
                perror("wait error");
                exit(-1);
            }
        }
        else if(ret == 0)
            continue;
        else
        printf("回收子进程<%d>, 终止状态<%d>\n", ret,WEXITSTATUS(status));
    }
    exit(0);
}

        使用了waitpid()函数执行非阻塞等待,可以实现轮询,它会不断地检查所有子进程是否已经退出。如果有子进程退出了,就会回收子进程并打印它的退出状态。如果没有子进程退出,则继续轮询。当waitpid返回值小于0时,表示发生错误,这时需要通过errno来判断是ECHILD还是其他错误,如果是ECHILD,则表示没有子进程需要回收了,可以直接退出程序。否则,使用perror函数打印错误信息并退出程序。而if (ret == 0) continue;这句代码的作用是,如果当前没有子进程退出,则直接跳过继续轮询的代码,进入下一轮循环,以减少不必要的系统开销

验证效果与上面wait()的一样

你可能感兴趣的:(LINUX,C编程,c语言,linux,ubuntu,unix)