Linux中fork()函数

本文介绍了Linux中的进程概念以及核心系统调用fork()的工作原理。进程是执行中的程序实例,当调用fork()时,会创建一个与父进程相似的子进程。父子进程之间不共享数据空间,但共享正文段。文章通过实例解释了fork()函数的返回值,父子进程执行顺序的不确定性,以及如何处理文件描述符。同时强调了在使用fork()时,应避免僵死进程的产生,父进程应通过wait或waitpid回收子进程资源。

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

一、什么是进程

进程是正在执行的程序实例。执行程序时,内核会将程序代码载入虚拟内存,为程序变量分配空间,在内核中建立相应的数据结构,以记录与进程有关的各种信息(比如,进程ID、用户ID、组ID以及终止状态等)。

简单来说,就是”执行一个程序或命令“就可以出发一个事件而获取一个进程ID。也就是说,程序被触发后,执行者的权限与属性、程序代码与数据等会被加载到内存,操作系统并给予这个内存单元一个标识符(进程ID)。

进程可以使用系统调用fork()来创建一个子进程。子进程获得父进程的数据空间、堆和栈的副本。父进程和子进程并不共享这些存储空间部分,共享正文段,也就是在内存中被标记为只读的程序文本段。例如在Linux shell中键入命令,ps时,shell会创建一个进程,这个子进程执行ps。

kernel@Ubuntu:~/Desktop$ ps -l
F S   UID    PID   PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  17637  17627  3  80   0 -  6909 wait   pts/1    00:00:00 bash
0 R  1000  17650  17637  0  80   0 -  3554 -      pts/1    00:00:00 ps

二、系统调用fork()

1. fork()函数被调用一次,但有两个返回值。父进程返回新创建的子进程ID,子进程返回0。此时的0不是进程ID为0的进程(ID为0的进程通常是调度进程,是内核的一部分,它并不执行任何磁盘上的进程,被称为系统进程)。

实例:

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

void err_sys(const char * fmt, ...) __attribute((noreturn));

int    globvar = 5;
char   buf[] = "global String\n";

int main(void) {

    int     var;
    pid_t   pid;

    var = 28;
    if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1)  /*将buf缓冲区的字符由标准输出输出到终端*/
        err_sys("write error");
    printf("before fork");

    if ((pid = fork()) < 0)    /*创建子进程*/
        err_sys("fork erro");
    if (!pid) {                /*执行子进程*/
        globvar++;
        var++;
    }
    else 
        sleep(2);              /*父进程等待子进程执行完成*/

    printf("pid = %d, ppid = %d, var = %d, globlvar = %d\n", getpid(), getppid(), var, globvar);

    exit(0);
}

结果如下:

kernel@Ubuntu:~/Desktop$ gcc -g fork.c -o fork
kernel@Ubuntu:~/Desktop$ ./fork 
global String
before fork
pid = 21398, ppid = 21397, var = 29, globlvar = 6
pid = 21397, ppid = 20518, var = 28, globlvar = 5

2.由结果可以看出,子进程改变了全局变量globvar和局部变量var的值,但对于父进程来说,却是没有改变的。所以说子进程是父进程创建的副本,它们并不共享存储空间的部分。而且在调用fork()之前,有”beforefork” 输出,而在创建子进程之后,子进程并没有输出该字符串。因为printf()是标准I/O函数,而且输出流是连接到终端,通常使用行缓冲。遇到换行符标准I/O执行I/O操作,将缓冲区的内容写到终端,并清理缓冲区。但是将标准输出重定向到一个文件时,在执行fork函数时,该行数据还在缓冲区,然后父进程将数据复制到子进程,该缓冲区的数据也被复制到子进程。

kernel@Ubuntu:~/Desktop$ ./fork > file
kernel@Ubuntu:~/Desktop$ cat file
global String
before fork
pid = 21493, ppid = 21492, var = 29, globlvar = 6
before fork
pid = 21492, ppid = 20518, var = 28, globlvar = 5

因为write是不带缓冲的,其数据只写到标准输出一次,所以子进程数据空间并没有write写入的数据。

3.在fork之后,父进程和子进程的执行顺序是不确定的,这取决于内核的调度算法。因此在pid> 0,父进程执行是休眠2s,让pid == 0时,子进程执行完毕后,再执行父进程。

还有一种情况,fork创建子进程,子进程通过调用exec的一系列函数,执行另一部分程序。

4.fork之后处理文件有两种方式

(1)父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾读、写操作的任一共享描述符的文件偏移量已经做了相应的更新。

#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include "err.h"

void err_sys(const char * fmt, ...) __attribute__((noreturn));

int main(void) {

   pid_t     pid;
   off_t     currpos;
   char      buf1[] = "testing the lseek\n";
   char      buf2[] = "child add the lseek\n";
   int       fd;

   if ((fd = open("/home/kernel/Desktop/testfile", O_RDWR | O_CREAT | O_TRUNC, )) < 0)        /* 读、写创建或打开文件 */
       err_sys("open error");

   if (write(fd, buf1, sizeof(buf1) - 1) != sizeof(buf1) - 1) 
       err_sys("write error");

   currpos = lseek(fd, 0, SEEK_CUR);                /* 当前文件偏移量 */
   printf("before fork the lseek is %ld\n", currpos);

   if ((pid = fork()) < 0) 
       err_sys("fork error");
   if (!pid) {
       if (write(fd, buf2, sizeof(buf2) - 1) != sizeof(buf2) - 1)   
           err_sys("write error");    
       currpos = lseek(fd, 0, SEEK_CUR);           /*子进程文件偏移量 */
       printf("now child lseek is %ld\n", currpos);         
   }
   else {
       sleep(2);
       currpos = lseek(fd, 0, SEEK_CUR);          /* 父进程文件偏移量 */
       printf("now parent lseek is %ld\n", currpos);
   }

   exit(0);
}

结果如下:

kernel@Ubuntu:~/Desktop$ gcc -g fork1.c -o fork1
kernel@Ubuntu:~/Desktop$ ./fork1
before fork the lseek is 18
now child lseek is 38
now parent lseek is 38

(2)父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭它们不需使用的文件描述符,就这样就不会干扰对方使用的描述符,这种方法是网络服务进程经常使用的。

5.现在可以看看父进程和子进程再去创建各自的子进程是什么情况。

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

void err_sys(const char * fmt, ...) __attribute__((noreturn));

int main(void) {

    pid_t    pid;
    int      i;

    for (i = 0; i < 2; i++) {
        if ((pid = fork()) < 0)
            err_sys("fork error");
        if (!pid) 
            printf("i = %d\tChild\tpid = %d\tppid = %d\n", i, getpid(), getppid());
        else 
            printf("i = %d\tParent\tpid = %d\tppid = %d\n", i, getpid(), getppid());
    }

    exit(0);

}
结果如下:

i = 0	Parent	pid = 14972	ppid = 12028
i = 0	Child	pid = 14973	ppid = 14972
i = 1	Parent	pid = 14972	ppid = 12028
i = 1	Parent	pid = 14973	ppid = 14972
i = 1	Child	pid = 14974	ppid = 14972
i = 1	Child	pid = 14975	ppid = 14973



结论:当 i = 0,parent0创建第一个子进程child0。i = 1,parent0与child0会创建各自的子进程child1与child2,此时child0也是一个父进程parent1,所以parent0与parent1会输出。i = 2时,parent0,child0/parent1,child1,child2结束循环并退出。

 三、总结

在使用fork函数时,子进程和父进程的执行顺序不定,有可能会出现竞争,有内核的调度算法实现。如果子进程比父进程先结束,并且父进程没有对子进程的资源进行回收,所以会产生僵死进程,并且kill其父进程,僵死进程也可能会还会存在。所以父进程应使用wait或waipid函数获取子进程相关信息,内核可以释放子进程占用的存储区,关闭打开的文件。如果父进程先于子进程结束,init进程会成为子进程的父进程,有init进程收养。

在循环中使用fork时,父进程创建子进程之后,下一次循环父进程还会继续执行与刚才相同的操作,且产生的子进程与上一次产生的子进程属于同一个父进程。上一次产生的子进程也会和父进程在下一次循环产生自己的子进程,直到循环结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值