从实例中深入理解fork

本文通过10个实例详细解析了fork函数,从基础到进阶,涵盖并发执行、进程独立性、atexit函数、僵死进程、wait函数、信号处理等多个知识点,帮助读者深入理解Linux进程创建及管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原共有17个fork实例,此日志从中挑选部分实例进行学习,由简入难

主函数

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

int main(int argc, char *argv[])
{
    int option = 0;
    if (argc > 1)
 option = atoi(argv[1]);
    switch(option) {
    case 0: fork0();
 break;
    case 1: fork1();
 break;
    case 2: fork2();
 break;
    case 3: fork3();
 break;
    case 4: fork4();
 break;
    case 5: fork5();
 break;
    case 6: fork6();
 break;
    case 7: fork7();
 break;
    case 8: fork8();
 break;
    case 9: fork9();
 break;
    case 10: fork10();
 break;
    case 11: fork11();
 break;
    case 12: fork12();
 break;
    case 13: fork13();
 break;
    case 14: fork14();
 break;
    case 15: fork15();
 break;
    case 16: fork16();
 break;
    case 17: fork17();
 break;
    default:
 printf("Unknown option %d\n", option);
 break;
    }
    return 0;
}

实例一

代码总览
void fork1()
{
    int x = 1;
    pid_t pid = fork();
    if (pid == 0) {
 printf("Child has x = %d\n", ++x);
    } 
    else {
 printf("Parent has x = %d\n", --x);
    }
    printf("Bye from process %d with x = %d\n", getpid(), x);
}
编译运行

在这里插入图片描述

知识点分析

这是一个简单的fork函数实例,以此来介绍fork基础知识

  • 调用一次,返回两次。fork函数被父进程调用一次,返回两次,一次返回到父进程,一次返回到新创建的子进程。父进程会返回子进程的pid,子进程返回0。
  • 并发进行。父进程和子进程是并发运行的独立进程。在我的系统上运行这个程序时,父进程先完成printf,然后是子进程,但在另一个系统上可能就正好相反了。
  • 相同但是独立的地址空间。父进程和子进程的地址空间都是相同的,但是他们都是独立的进程,都有自己私有的地址空间,对各自的变量x做出的改变是独立的。这就是为什么两个进程调用printf函数后,它们中的变量x会有不同的值。
  • getpid函数。调用此函数后可以得到当前进程的pid。
代码分析

若先进入子进程,此时x=1,进行++x操作后x=2,但此操作不会对父进程中的x造成影响,故进入父进程后,此时依旧是x=1,进行- -x操作后x=0,从而出现以上图示结果。

实例二

代码总览
void fork2()
{
    printf("L0\n");
    fork();
    printf("L1\n");    
    fork();
    printf("Bye\n");
}
编译运行

在这里插入图片描述

代码分析

这个例子中调用了多个fork函数,这时我们会画fork进程图来分析结果。如下所示:
在这里插入图片描述
由于父进程和子进程的并发进行,这个程序在不同的系统中会有多种不同的运行结果,如“L0 L1 L1 Bye Bye Bye Bye”也是可能的序列,但"L0"不可能在"L1"和"Bye"后面输出。

实例三

代码总览
void fork4()
{
    printf("L0\n");
    if (fork() != 0) {
 printf("L1\n");    
 if (fork() != 0) {
     printf("L2\n");
 }
    }
    printf("Bye\n");
}
编译运行

在这里插入图片描述

代码分析

fork进程图如下:
在这里插入图片描述
此程序中用 if (fork() != 0) 判断正在进行的进程是否是父进程(父进程进行时fork返回非0数),若不是父进程则直接进行最后一个printf函数的调用,具体情况如进程图所示。此程序也有多种运行结果。

实例四

代码总览
void cleanup(void) {
    printf("Cleaning up\n");
}

void fork6()
{
    atexit(cleanup);
    fork();
    exit(0);
}
编译运行

在这里插入图片描述

知识点分析
  • atexit函数:它是在正常程序退出时调用的函数,即main执行结束后调用的函数。
代码分析

调用fork函数后,父进程结束后会调用atexit函数,从而调用cleanup函数输出“Cleaning up”,子进程亦然,故输出两行“Cleaning up”。

实例五

代码总览
void fork7()
{
    if (fork() == 0) {
 /* Child */
 printf("Terminating Child, PID = %d\n", getpid());
 exit(0);
    } else {
 printf("Running Parent, PID = %d\n", getpid());
 while (1); /* Infinite loop */
    }
}
编译运行

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码分析

命令行中输入“ps”可以查看所有正在运行的程序
此程序在进入父进程后无限循环,有两个解决方法。

  • 按ctrl+c直接关闭程序,利用shell回收父进程。如图一。
  • 按ctrl+z停止运行程序,并用命令行“kill -9 PID”(PID为需要杀死的程序的pid)杀死父进程。如图二图三。

实例六

代码总览
void fork8()
{
    if (fork() == 0) {
 /* Child */
 printf("Running Child, PID = %d\n",
        getpid());
 while (1); /* Infinite loop */
    } else {
 printf("Terminating Parent, PID = %d\n",
        getpid());
 exit(0);
    }
}
编译运行

在这里插入图片描述
在这里插入图片描述

知识点分析
  • 僵死进程。当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除,相反,进程被保持在一个已终止的状态中,直到被它的父进程回收。一个被终止了但还未被回收的进程称为僵死进程
代码分析

这个程序与上个程序不同的地方在于父进程退出了,子进程进入了死循环。

  • 此时父进程已经退出无法回收子进程,且无法用ctrl+c或ctrl+z解决问题。
  • 只能用kill命令行杀死子程序。
  • 为了防止这种情况的发生,我们可以用到wait函数,我们会在下一个实例中看到它的应用。

实例七

代码总览
void fork9()
{
    int child_status;
    if (fork() == 0) {
 printf("HC: hello from child\n");
        exit(0);
    } else {
 printf("HP: hello from parent\n");
 wait(&child_status);
 printf("CT: child has terminated\n");
    }
    printf("Bye\n");
}
编译运行

在这里插入图片描述

代码分析

fork进程图如下:

在这里插入图片描述

  • 一个进程可以调用wait(或waitpid)函数来等待它的子进程终止或者停止。
  • 在这个程序中,子进程没有退出前,程序不会执行 printf(“CT: child has terminated\n”); 语句。
  • 父进程会等子进程结束后,回收子进程,再执行后续操作。

实例八

代码总览
void fork11()
{
    pid_t pid[N];
    int i;
    int child_status;
    for (i = 0; i < N; i++)
 if ((pid[i] = fork()) == 0)
     exit(100+i); /* Child */
    for (i = N-1; i >= 0; i--) {
 pid_t wpid = waitpid(pid[i], &child_status, 0);
 if (WIFEXITED(child_status))
     printf("Child %d terminated with exit status %d\n",
     wpid, WEXITSTATUS(child_status));
 else
     printf("Child %d terminate abnormally\n", wpid);
    }
}
void fork12()
{
    pid_t pid[N];
    int i;
    int child_status;
    for (i = 0; i < N; i++)
 if ((pid[i] = fork()) == 0) {
     /* Child: Infinite Loop */
     while(1);
 }
    for (i = 0; i < N; i++) {
 printf("Killing process %d\n", pid[i]);
 kill(pid[i], SIGINT);
    }
    for (i = 0; i < N; i++) {
 pid_t wpid = wait(&child_status);
 if (WIFEXITED(child_status))
     printf("Child %d terminated with exit status %d\n",
     wpid, WEXITSTATUS(child_status));
 else
     printf("Child %d terminated abnormally\n", wpid);
    }
}
编译运行

fork11
在这里插入图片描述
fork12
在这里插入图片描述

知识点分析
  • WIFEXITED(status):如果子进程通过调用exit或者一个返回(return)正常终止,就返回真。

  • WEXITSTATUS(status):返回一个正常终止的子进程的退出状态。只有在WIFEXITED( )返回为真时,才定义这个状态。

  • waitpid函数
    pid_t waitpid(pid_t pid,int *statusp,int options);
    如果成功,则返回子进程的pid;如果等待集合中的任何子进程都还没有结束,则立即返回0,默认挂起程序;如果其他错误,则返回-1。

  • kill函数
    int kill(pid_t pid,int sig)
    如果pid>0,则kill函数发送信号号码sig给进程pid;pid=0,则发送信号号码sig给调用进程所在进程组中的每个进程;pid<0,则发送信号号码sig给进程组 | pid | 中的每个进程。

  • 信号在此只列举几个常见的信号

序号名称相应事件
2SIGINT来自键盘的中断
9SIGKILL杀死程序
14SIGALRM来自alarm函数的定时器信号
17SIGCHLD一个子进程停止或终止
代码分析
  • 在fork11中,子进程在for循环中调用exit结束子进程,并且每个退出状态都有一个与之匹配的标签,调用“WEXITSTATUS(status)”会返回子进程退出时的标签。这是子进程的正常退出。
  • 在fork12中,子进程陷入死循环而无法退出,调用kill函数发出信号SIGINT中断进程,这是子进程的异常中断。

实例九

代码总览
void int_handler(int sig)
{
    printf("Process %d received signal %d\n", getpid(), sig); /* Unsafe */
    exit(0);
}
void fork13()
{
    pid_t pid[N];
    int i;
    int child_status;
    signal(SIGINT, int_handler);
    for (i = 0; i < N; i++)
 	if ((pid[i] = fork()) == 0) {
     /* Child: Infinite Loop */
     while(1);
 }
    for (i = 0; i < N; i++) {
 	printf("Killing process %d\n", pid[i]);
 	kill(pid[i], SIGINT);
    }
    for (i = 0; i < N; i++) {
 	pid_t wpid = wait(&child_status);
 	if (WIFEXITED(child_status))
     	printf("Child %d terminated with exit status %d\n",
     	wpid, WEXITSTATUS(child_status));
 	else
    	printf("Child %d terminated abnormally\n", wpid);
    }
}
编译运行

在这里插入图片描述

知识点分析
  • signal函数
    sighandler_t signal(int signum, sighandler_t handler);
    这个函数被称为信号处理程序,只要进程收到一个类型为signum的信号,就会调用这个handler这个程序。
代码分析

所有的子进程进入死循环后,调用kill函数给每个子进程发送了SIGINT信号,signal函数接收到信号后,调用int_handler函数,让每个子进程都以标签0正常退出了,故运行结果为上图。

实例十

代码总览
void fork16() 
{
    if (fork() == 0) {
 printf("Child1: pid=%d pgrp=%d\n",
        getpid(), getpgrp());
 if (fork() == 0)
     printf("Child2: pid=%d pgrp=%d\n",
     getpid(), getpgrp());
 while(1);
    }
} 
编译运行

在这里插入图片描述
在这里插入图片描述

知识点分析
  • getpgrp函数:每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。此函数将返回当前进程的进程组ID。
代码分析

这个程序中的两个子进程都属于一个进程组,使用命令行 kill -9 -进程组ID 可以同时杀死此进程组中的所有子进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值