Linux进程的fork、exit、wait等函数;区分父子进程;GDB调试多进程

Linux系统中进程可以创建子进程。

1. fork函数:创建新进程

#include<sys/types.h>
#include<unistd.h>

pid_t fork(void);
/*
功能:
    一个进程创建新进程。原进程为父进程,新进程为子进程。
返回值:
    成功:
        子进程中返回0,父进程中返回子进程的pid。
    失败:-1
    失败原因:
        a) 进程总数达到系统上限,此时errno被设置为EAGAIN
        b) 系统内存不足,此时errno被设置为ENOMEM
*/

fork示例:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>


int test01() {
    fork(); // fork成功的话,给子进程返回0,给父进程返回子进程的pid
    printf("Hello world\n");
}

运行结果:

pc指针:指向当前运行指令的下一条指令。

fork时,父进程的pc指针也会复制,因此子进程会从fork后的指令开始执行。


2. exit和_exit函数:结束进程

#include<stdlib.h>

void exit(int status);  // 标准库函数

/*******************************************/

#include<unistd.h>

void _exit(int status);  // 系统调用
/*
功能:
    结束调用此函数的进程,并将函数的退出状态放在status中。
参数:
    status:返回给父进程的参数(低8位有效),
            此参数根据需求填写;
            例如,写123,则正常退出时会传递状态码123;
            若被信号终止,则传递的退出码就是信号的编号,而不再是123.
*/

exit和_exit的区别:

exit会刷新缓冲区、关闭文件描述符:

  exit和_exit区别示例:

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


int main(int argc, const char* argv[]) {
    printf("Hello.");

    _exit(0); // 什么也不输出。
    //exit(0); // 输出Hello. exit会刷新缓冲区
    //return 0; // 输出Hello. return 0也会刷新缓冲区
}
/* 
注:若printf中有\n,则_exit(0)也会输出Hello. 
因为\n在标准输出中具有刷新缓冲区的作用.
*/

return也可结束进程,return和exit的区别是:

(1)若一个进程先后调用funcA,funcB。

         若funcA中使用exit结束,则该进程立即结束,不会再调用funcB;

         若funcA中使用return结束,则表示该funcA函数结束,仍会继续调用funcB.

(2)return退出进程时,不会关闭文件描述符、刷新缓冲区等,而exit可以。


3. wait和waitpid

进程退出时,内核释放该进程大部分资源,包括打开的文件、占用的内存等。但仍保留了该进程的PCB信息,因此需要父进程通过wait和waitpid函数来进一步回收,否则这些进程会变成僵尸进程,消耗系统资源。

(1)wait函数:

  阻塞等待子进程退出,回收子进程资源。

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status);
/*
功能:
    等待任意一个子进程结束,回收该子进程资源,并传出子进程退出的状态到status。
参数:
    status:存储进程退出时的状态信息;NULL表示不关心子进程退出状态。
返回值:
    成功:被回收的子进程号
    失败:-1
*/

设置退出时的状态信息status使用方式:

(1)若WIFEXITED(status)非0:
        表示进程正常退出,可使用WEXITSTATUS(status)获取进程退出状态码(即exit的参数)。
(2)若WIFSIGNALED(status)非0:
        表示进程异常终止(被信号杀死),可使用WTERMSIG(status)获取终止信号的编号。
(3)若WIFSTOPPED(status)非0:
        表示进程被暂停,可使用WSTOPSIG(status)获取暂停信号的编号。
(4)若WIFCONTINUED(status)非0:
        表示进程暂停后被继续运行。

调用wait函数的进程会被阻塞,直到有一个子进程退出或收到一个不能被忽视的信号。

若调用wait的进程无子进程,wait函数会立即返回;若子进程早就结束,则wait函数也会立即返回,并且回收该早就结束的子进程。

若参数status的值不为NULL,wai函数会把子进程退出时的状态(int型数值)存入status,指出子进程是否为正常结束。该退出的状态信息。

wait和获取子进程退出状态信息示例:

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

int main(int argc, const char* argv[]) {
    pid_t pid = -1;
    int ret = -1;
    int status = 0;

    pid = fork();
    if (-1 == pid) {
        perror("fork");
        return 1;
    }

    if (0 == pid) { // 子进程
        printf("子进程%d运行...\n", getpid());
        sleep(10);
        exit(10); // 子进程终止。指定状态码为10.
    }

    // 父进程
    printf("父进程执行。等待子进程退出,回收其资源...\n");

    // 父进程阻塞,等待子进程退出
    ret = wait(&status);
    if (-1 == ret) {
        perror("wait");
        return 1;
    }
    printf("父进程回收子进程%d的资源...\n", ret);

    // 获取子进程退出的状态
    if (WIFEXITED(status)) {  // 子进程正常退出
        printf("子进程正常退出,状态码:%d\n", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        printf("子进程被信号%d杀死了...\n", WTERMSIG(status));
    } else if (WIFSTOPPED(status)) {
        printf("子进程被信号%d暂停...\n", WSTOPSIG(status));
    }
    return 0;
}

 正常退出,显示指定的状态码10,运行结果:

 在另一个终端使用kill -9杀死该进程,运行结果:

在另一个终端直接使用kill杀死该进程,运行结果:

在另一个终端直接使用kill -19暂停该进程,运行结果:

 (2)waitpid函数:

 阻塞等待子进程退出,回收子进程资源;也可设置非阻塞,无子进程退出则立即返回。

#include<sys/types.h>
#include<sys/wait.h>

pid_t waitpid(pid_t pid, int* status, int options);
/*
功能:
    等待子进程结束,回收该子进程资源,并传出子进程退出的状态到status。
参数:
    pid:
        > 0:等待进程号为pid的子进程退出;
        = 0:等待同一个进程组中的任何子进程退出;若子进程已加入其他进程组,则不会等待;
        = -1:等待任意一个子进程退出,此时和等价于wait函数;
        < -1:等待进程组号为pid绝对值的进程组中的任何子进程退出。

    status:存储进程退出时的状态信息;NULL表示不关心子进程退出状态。

    options:
        0:阻塞父进程,等待子进程退出。此时同wait函数。
        WNOHANG:若无任何子进程退出,则立即返回。
        WUNTRACED:若子进程暂停,则立即返回。(少用)
返回值:
    a) 有子进程退出时,waitpid返回已收集到的退出子进程的进程号;
    b) 若options设为WNOHANG,调用waitpid时无子进程退出,则返回0;
    c) 若调用中出错,返回-1,同时设置errno. 
        如当pid对应的进程不存在,或pid对应的进程不是调用waitpid进程的子进程,就会出错,此时errno        
        被设为ECHILD。
*/    

 waitpid使用示例:

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

int main(int argc, const char* argv[]) {
    pid_t pid = -1;
    int ret = -1;
    int status = 0;

    pid = fork();
    if (-1 == pid) {
        perror("fork");
        return 1;
    }

    if (0 == pid) { // 子进程
        printf("子进程%d运行...\n", getpid());
        sleep(10);
        exit(10); // 子进程终止
    }

    // 父进程
    printf("父进程执行。等待子进程退出,回收其资源...\n");

    // 父进程阻塞,等待子进程退出
    // ret = waitpid(-1, &status, 0); // 此时等价于wait
    ret = waitpid(-1, &status, WNOHANG); // 设置非阻塞
    if (-1 == ret) {
        perror("wait");
        return 1;
    }

    if (0 == ret) {
        printf("暂无子进程退出,waitpid直接返回.\n");
    } else {
        printf("父进程回收子进程%d的资源...\n", ret);
    }

    return 0;
}

 运行结果:

 26行添加sleep(11)后,运行结果:


4. 区分父子进程

通过fork的返回值区分:

若fork成功,则在子进程中返回0,父进程中返回子进程的pid。

示例:

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

int main(int argc, const char* argv[]) {
    pid_t pid = -1;
    int status = 0;
    int ret = -1;

    // 创建子进程。若创建成功,则在子进程中返回0,父进程中返回子进程的pid
    pid = fork();
    if (0 < pid) {
        perror("fork");
        return 1;
    }
    
    if (0 == pid) {  // 子进程
        printf("这是子进程。进程号 = %d, 父进程号 = %d\n", getpid(), getppid());
        exit(0);  // 退出子进程,或者return。
    } else {  // 父进程
        printf("这是父进程。进程号 = %d, 子进程号 = %d\n", getpid(), pid);
    }
    
    ret = wait(&status); // 父进程等待回收子进程资源
    if(-1 == ret) {
        perror("wait");
        return 1;
    }
    
    return 0;
}

 运行结果:


5. 父子进程关系

(1)写时拷贝(copy-on-write)。读时共享、写时拷贝。父子进程未修改某变量时,无需拷贝;当父子进程之一修改该变量时,子进程就拷贝一份。

(2)在fork之前、之后open对文件描述信息的影响:

  • 在fork前open,父子进程共享一个文件描述信息,包括引用计数、文件偏移等等。子进程复制了父进程的文件表项指针,指向的是同一个文件表项。
  • 在fork后open,父子进程各自有自己的文件描述信息,互不影响。一个文件被打开了两次,即引用计数值为2,每个进程都有自己的一份,文件偏移也互不影响。

简单来说,就是先open再fork,文件描述信息是共享的;先fork再open,文件描述信息是独立的。

文件描述信息是内核为每个进程维护的一个文件描述符表,fork时文件描述信息不会被子进程复制,而是被共享(内核空间被所有进程共享)。因此fork之前open、read、write等操作改变了文件偏移,fork之后子进程会从改变了的文件偏移位置继续操作。


6. 父子进程堆区内存开辟释放问题

父子进程都要释放各自堆区的内存。

如下程序父子进程未释放堆区内存,有内存泄漏问题:

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

int main(int argc, const char* argv[]) {
    pid_t pid = -1;
    int ret = -1;
    int status = 0;
    int* p = malloc(sizeof(int)); 
    (*p) = 1;

    pid = fork();
    if (-1 == pid) {
        perror("fork");
        return 1;
    }

    if (pid > 0) {
        // 父进程
        (*p) = 3;
        printf("父进程(*p) = %d\n", (*p));
    } else {
        // 子进程
        sleep(1);
        printf("子进程(*p) = %d\n", (*p));
        exit(0);
    }

    ret = wait(&status); // 父进程回收子进程资源
    if(-1 == ret) {
        perror("wait");
        return 1;
    }
    
    return 0;
}

运行结果:

但存在内存泄漏问题。

valgrind查看内存泄漏情况:

有内存泄漏。 

 修改:在父子进程退出前释放各自的堆区内存

free(p);
p = NULL;

再次查看内存泄漏情况:无内存泄漏。


补充: GDB调试多进程

GDB调试

有如下的多进程程序,需要使用GDB分别调试父子进程:

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

int main(int argc, const char* argv[]) {
    pid_t pid = -1;
    int status = 0;
    int ret = -1;

    pid = fork();
    if (-1 == pid) {
        perror("fork");
        return 1;
    }

    if (pid > 0) {
        // 父进程
        printf("父进程说太强了");
        printf("父进程笑尿了");
        printf("父进程哈哈哈哈哈");
    } else {
        // 子进程
        printf("子进程说太强了");
        printf("子进程笑尿了");
        printf("子进程哈哈哈哈哈");
        exit(0);
    }
    
    ret = wait(&status); // 父进程等待回收子进程资源
    if(-1 == ret) {
        perror("wait");
        return 1;
    }

    return 0;
}

GDB调试默认跟踪父进程,如下:

如何跟踪子进程呢?

在fork函数调用之前设置跟踪子进程:

set follow-fork-mode child

然后就会跟踪子进程,如下:

  • 0
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

伟大的马师兄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值