浅谈Linux环境下并发编程中C语言fork()函数的使用

由fork创建的新进程被称为子进程(child process)。fork函数被调用一次,但返回两次。子进程的返回值是0,而父进程的返回值则是新进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getpid以获得其父进程的进程ID。
使fork失败的两个主要原因是:系统中已经有了太多的进程,或者该实际用户ID的进程总数超过了系统限制。

fork有下面两种用法:

(1)一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的--父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。

(2)一个进程要执行一个不同的程序。这对shell是常见的情况。子进程从fork返回后立即调用exec。

归结起来说就是是实现多线程。C语言多线程实现需要自己控制来实现,这个比Java要复杂。

注意:fork确实创建了一个子进程并完全复制父进程,但是子进程是从fork后面那个指令开始执行的。对于原因也很合乎逻辑,如果子进程也从main开头到尾执行所有指令,那么它执行到fork指令时也必定会创建一个子子进程,子子孙孙无穷尽也,如此下去,这个小小的程序就可以创建无数多个进程可以把你的电脑搞瘫痪,所以fork作者肯定不会这么做。

原来刚刚开始做Linux下面的多进程编程的时候,对于下面这段代码感到很奇怪,

#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<stdarg.h>
#include<errno.h>
#define LEN 2
void err_exit(char *fmt,...);
int main(int argc,char *argv[])
{
  pid_t pid;
  int loop;

  for(loop=0;loop<LEN;loop++)
  {
  if((pid=fork()) < 0)
    err_exit("[fork:%d]: ",loop);
  else if(pid == 0)
  {
   printf("Child process\n");
  }
  else
  {
    sleep(5);
  }
  }

  return 0;
}

为什么这段程序会创建3个子进程,而不是两个,为什么在第20行后面加上一个return 0;就创建的又是两个子进程了?原来一直搞不明白,后来了解了C语言程序的存储空间布局以及在fork之后父子进程是共享正文段(代码段CS)之后才明白这其中的缘由!具体原理是啥,且容我慢慢道来!

首先得明白一个东西就是C程序的存储空间布局,如下图所示:

当一个C程序执行之后,它会被加载到内存之中,它在内存中的布局如上图,分为这么几个部分,环境变量和命令行参数、栈、堆、数据段(初始化和未初始化的)、正文段,下面挨个来说明这几段分别代表了什么:

环境变量和命令行参数:这些指的就是Unix系统上的环境变量(比如$PATH)和传给main函数的参数(argv指针所指向的内容)。

数据段:这个是指在C程序中定义的全局变量,如果没有初始化,那么就存放在未初始化的数据段中,程序运行时统一由exec赋值为0。否则就存放在初始化的数据段中,程序运行时由exec统一从程序文件中读取。(了解汇编的朋友们想必知道汇编语言中的数据段DS,这和汇编中的数据段其实是一个东西)。

堆:这一部分主要用来动态分配空间。比如在C语言中用malloc申请的空间就是在这个区域申请的。

正文段:C语言代码并不是直接执行的,而是被编译成了机器指令才能够在电脑上执行,最终生成的机器指令就是存放在这个区域(汇编中的代码段CS指的就是这片区域)。

栈:个人感觉这是C程序内存布局最关键的部分了。这个部分主要用来做函数调用。具体而言怎么说呢,程序刚开始栈中只有main这一个函数的内容(即main的栈帧),如果main函数要调用func函数,那么func函数的返回地址(main函数的地址),func函数的参数,func函数中定义的局部变量,还有func函数的返回值等等这些都会被压入栈中,这时栈中就多了func函数的内容(func的栈帧)。然后func函数运行完了之后再来弹栈,把它原来压的内容去掉(即清除掉func栈帧),此时栈中又只剩下了main的栈帧。(这片区域就是汇编中的栈段SS)

OK,这就是C程序的存储器布局。这里我联想到另外一点,就是全局变量和静态变量是存储在数据段中的,而局部变量是存储在栈中的,栈中数据在函数调用完之后一弹栈就没了,这就是为什么全局变量的生存周期比局部变量的生存周期要长的原因。

了解了C程序在存储器的布局之后,我们再来了解fork的内存复制机制,关于这个,我们只需要了解一句话就够了,“子进程复制父进程的数据空间(数据段)、栈和堆,父、子进程共享正文段。”也就是说,对于程序中的数据,子进程要复制一份,但是对于指令,子进程并不复制而是和父进程共享。具体来看下面这段代码(这是我在上面那段代码上稍微添加了一点东西):

/* 这个程序会创建3个子进程,理解这句话,父子进程复制数据段、栈、堆,共享正文段
 *
 */
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<stdarg.h>
#include<errno.h>
#define BUFSIZE 512
#define LEN 2
void err_exit(char *fmt,...);
int main(int argc,char *argv[])
{
  pid_t pid;
  int loop;

  for(loop=0;loop<LEN;loop++)
  {
  printf("Now is No.%d loop:\n",loop);

  if((pid=fork()) < 0)
    err_exit("[fork:%d]: ",loop);
  else if(pid == 0)
  {
   printf("[Child process]P:%d C:%d\n",getpid(),getppid());
  }
  else
  {
    sleep(5);
  }
  }

  return 0;
}

为什么上面那段代码会创建三个子进程?我们来具体分析一下它的执行过程:

首先父进程执行循环,通过fork创建一个子进程,然后sleep5秒。

再来看父进程创建的这个子进程,这里我们记为子进程1.子进程1完全复制了这个父进程的数据部分,但是需要注意的是它的正文段是和父进程共享的。也就是说,子进程1开始执行代码的部分并不是从main的 { 开始执行的,而是主函数执行到哪里了,它就接着执行,具体而言就是它会执行fork后面的代码。所以子进程1首先会打印出它的ID和它的父进程的ID。然后继续第二遍循环,然后这个子进程1再来创建一个子进程,我们记为子进程11,子进程1开始sleep。

子进程11接着子进程1执行的代码开始执行(即fork后面),它也是打印出它的ID和父进程ID(子进程1),然后此时loop的值再加1就等于2了,所以子进程2直接就返回了。

那个子进程1sleep完了之后也是loop的值加1之后变成了2,所以子进程1也返回了!

然后我们再返回去看父进程,它仅仅循环了一次,sleep完之后再来进行第二次循环,这次又创建了一个子进程我们记为子进程2。然后父进程开始sleep,sleep完了之后也结束了。

那么那个子进程2怎么样了呢?它从fork后开始执行,此时loop等于1,它打印完它的ID和父进程ID之后,就结束循环了,整个子进程2就直接结束了!

这就是上面那段代码的运行流程,进程间的关系如下图所示:

上图中那个loop=%d就是当这个进程开始执行的时候loop的值。上面那段代码的运行结果如下图:

这里这个3498进程就是我们的主进程,3499就是子进程1,3500就是子进程11,3501就是子进程2。

最后,我们再来回答一下我们开始的时候提出的那个问题,为什么在子进程的处理部分“ if(pid == 0) ”最后加一个return 0,就会创建两个子进程了,就是因为子进程1运行到这里直接就结束了,不再进行第二遍循环了,所以就不会再去创建那个子进程11了,所以最后一共就是创建了两个子进程啊!

(0)

相关推荐

  • C语言的fork函数在Linux中的进程操作及相关面试题讲解

    fork的意义 下图为,C 程序的存储空间布局(典型) 1.一个现有进程可以调用 fork 函数创建一个新进程. 2.fork 函数被调用一次,但返回两次, 两次返回的唯一区别是子进程的返回值是 0, 而父进程的返回值是新子进程的 PID. 3.子进程和父进程继续执行 fork 调用之后的指令. 在上图的存储空间布局中,父子进程只共享正文段,其余的都各自有独立的副本 (通常使用 copy-on-write 的策略,速度比较快). fork 的两种用法 1.父子进程同时执行不同的代码段 典型应用:

  • 使用C语言的fork()函数在Linux中创建进程的实例讲解

    在Linux中创建一个新进程的唯一方法是使用fork()函数.fork()函数是Linux中一个非常重要的函数,和以往遇到的函数有一些区别,因为fork()函数看起来执行一次却返回两个值. fork()函数用于从已存在的进程中创建一个新进程.新进程称为子进程,而园进程称为父进程.使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程的上下文.代码段.进程堆栈.内存信息.打开的文件描述符.符号控制设定.进程优先级.进程组号.当前工作目录.根目录.资源限

  • Linux中fork()函数实例分析

    一.fork 入门知识 一个进程,包括代码.数据和分配给进程的资源.fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事. 一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间.然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同.相当于克隆了一个自己. 我们来看一个例子: /* * fork_test.c * version 1 * C

  • Linux 编程之进程fork()详解及实例

    Linux fork()详解: 在开始之前,我们先来了解一些基本的概念: 1. 程序, 没有在运行的可执行文件 进程,  运行中的程序 2. 进程调度的方法: 按时间片轮转       先来先服务      短时间优先      按优先级别 3. 进程的状态: 就绪   ->>   运行  ->> 等待          运行 ->> 就绪 //时间片完了          等待 ->> 就绪 //等待的条件完成了 查看当前系统进程的状态 ps auxf s

  • Linux下C语言的fork()子进程函数用法及相关问题解析

    fork fork()函数是linux下的一个系统调用,它的作用是产生一个子进程,子进程是当前进程的一个副本,它跟父进程有一样的虚存内容,但也有一些不同点. 但是,值得注意的是,父进程调用fork()后,fork()返回的是生成的子进程(如果能顺利生成的话)的ID.子进程执行的起点也是代码中fork的位置,不同的是下面这段C语言代码展示了fork()函数的使用方法: // myfork.c #include <unistd.h> #include <stdio.h> int mai

  • Linux 中fork的执行的实例详解

    Linux 中fork的执行的实例详解 先看看一段fork的程序 int main() { pid_t pid; 语句 a; pid = fork(); 语句 b; } 1.当程序运行到 pid = fork()时,这个进程马上分裂(fork的中文意思)成两个进程,我们称为父进程和子进程,子进程是父进程的副本,副本的意思是子进程把父进程的数据空间,堆和栈都复制一遍给自己用,这要求在内存给子进程分配和父进程同样大的存储空间,这样,父,子进程拥有相同的数据,但不会共享存储空间,他们只是共享正文段.

  • Linux系统中C语言编程创建函数fork()执行解析

    最近在看进程间的通信,看到了fork()函数,虽然以前用过,这次经过思考加深了理解.现总结如下: 1.函数本身 (1)头文件 #include<unistd.h> #include<sys/types.h> (2)函数原型 pid_t fork( void); (pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中) 返回值: 若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID:否则,出错返回-1 (3)函数说明 一

  • 详解linux中fork、vfork、clone函数的区别

    在linux系统中,fork(),vfork()和clone函数都可以创建一个进程,但是它们的区别是什么呢???本文就这三者做一个较深入的分析!!! 1.fork() fork()函数的作用是创建一个新进程,由fork创建的进程称为子进程,fork函数调用一次返回两次,子进程返回值为0,父进程返回子进程的进程ID.我们知道,一个进程的地 址空间主要由代码段,数据段,堆和栈构成,那么p2就要复制相关的段到物理内存.原始的unix系统的实现的是一种傻 瓜式的进程创建,这些复制包括: (1) 为子进程

  • 简单掌握Linux系统中fork()函数创建子进程的用法

    fork()函数用于从已存在的进程中创建一个新进程.新进程称为子进程,而园进程称为父进程.使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程的上下文.代码段.进程堆栈.内存信息.打开的文件描述符.符号控制设定.进程优先级.进程组号.当前工作目录.根目录.资源限制和控制终端等,而子进程所独有的只有它的进程号.资源使用和计时器等. 因为子进程几乎是父进程的完全复制,所以父子两进程会运行同一个程序.这就需要用一种方式来区分它们,并使它们照此运行,否则,

  • Linux中使用C语言的fork()函数创建子进程的实例教程

    一.fork入门知识 一个进程,包括代码.数据和分配给进程的资源.fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事. 一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间.然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同.相当于克隆了一个自己.   我们来看一个例子: #include <unistd.h> #include &

随机推荐