Linux fork()函数学习

来源:互联网 发布:济南联通软件研究院 编辑:程序博客网 时间:2024/04/27 16:21

在 UNIX 系统中,用户创建一个新进程的唯一方法就是调用系统调用 fork。调 用 fork 的进程称为父进程,而新创建的进程叫做子进程。系统调用的语法格式:

    pid = fork();

    在从系统调用 fork 中返回时,两个进程除了返回值 pid 不同外,具有完全一样的用户级上下文。在子进程中,pid 的值为零。在系统启动时由核心内 部地创建的进程0是唯一不通过系统调用 fork 而创建的进程。

    核心为系统调用 fork 完成下列操作:

  1. 为新进程在进程表中分配一个空项。
  2. 为子进程赋一个唯一的进程标识号 (PID)。
  3. 做一个父进程上下文的逻辑副本。由于进程的某些部分,如正文区,可能被几个 进程所共享,所以核心有时只要增加某个区的引用数即可,而不是真的将该区拷贝到一个 新的内存物理区。
  4. 增加与该进程相关联的文件表和索引节点表的引用数。
  5. 对父进程返回子进程的进程号,对子进程返回零。

    理解系统调用 fork 的实现是十分重要的,因为子进程就象从天而降一样地开始 它的执行序列。

    下面是系统调用 fork 的算法。核心首先确信有足够的资源来成功完成 fork。 如果资源不满足要求,则系统调用 fork 失败。如果资源满足要求,核心在进程 表中找一个空项,并开始构造子进程的上下文。


算法:fork
输入:无
输出:对父进程是子进程的 PID
  对子进程是0
{
检查可用的核心资源
取一个空闲的进程表项和唯一的 PID 号
检查用户没有过多的运行进程
将子进程的状态设置为“创建”状态
将父进程的进程表中的数据拷贝到子进程表中
当前目录的索引节点和改变的根目录(如果可以)的引用数加1
文件表中的打开文件的引用数加1
在内存中作父进程上下文的拷贝
在子进程的系统级上下文中压入虚设系统级上下文层
/* 虚设上下文层中含有使子进程能
* 识别自己的数据,并使子进程被调度时
* 从这里开始运行
*/
if (正在执行的进程是父进程) {
将子进程的状态设置为“就绪”状态
return (子进程的 PID) // 从系统到用户
}
else {
初始化计时区
return 0;
}
}

    我们来看看下面的例子。该程序说明的是经过系统调用 fork 之后,对文件的共享存取。用户调用该程序时应有两个参数,一个是已经有的文件名,另外一个是要创建的新文件名。该进程打开已有的文件,创建一个新文件,然后,假定没有遇见过 错误,它调用 fork 来创建一个子进程。子进程可以通过使用相同的文件描述 符而继承地存取父进程的文件(即父进程已经打开和创建的文件)。

    当然,父进程和子进程要分别独立地调用rdwrt 函数,并执行一个循环,即从 源文件中读一个字节,然后写一个字节到目标文件中区。当系统调用read 遇见 文件尾时,函数rdwrt 立即返回。


#include <fcntl.h>
int      fdrd, fdwt;
char  c;

main(int argc, char *argv[])
{
if (argc != 3) {
exit(1);
}
if ((fdrd = open(argv[1], O_RDONLY)) == -1) {
exit(1);
}
if ((fdwt = creat(argv[2], 0666)) == -1) {
exit(1);
}

fork();
// 两个进程执行同样的代码
rdwrt();
exit(0);
}

rdwrt()
{
for (;;) {
if (read(fdrd, &c, 1) != 1) {
return ;
}
write(fdwt, &c, 1);
}
}

    在这个例子中,两个进程的文件描述符都指向相同的文件表项。这两个进程永远不会读或写到相同的文件偏移量,因为核心在每次read 和write 调用之后,都要增加文件的偏移量。尽管两个进程似乎是将源文件拷贝了两次,但因为 他们分担了工作任务,因此,目标文件的内容依赖于核心调度两个进程的次序。如果 核心这样调度两个进程:使他们交替地执行他们的系统调用,或甚至使他们交替地执行每对read 和write 调用,则目标文件的内容和源文件的内容完全一致。但考虑这样的情况:两个进程正要读源文件中的两个连续的字符 "ab"。假定父进程读了字符 "a",这时,核心在父进程写之前,做了上下文切换来执行子进程。如果子进程读到字符 "b",并在父进程被调度前,将它写到目标文件,那么目标文件将不再含有字符串"ab",而是含有"ba"了。核心并不保证进程执行的相对速率。

再来看看另外一个例子:


#include <string.h>
char string[] = "Hello, world";
main()
{
int count, i;
int to_par[2], to_chil[2]; // 到父、子进程的管道
char buf[256];

pipe(to_par);
pipe(to_chil);

if (fork() == 0) {
// 子进程在此执行
close(0); // 关闭老的标准输入
dup(to_child[0]); // 将管道的读复制到标准输入
close(1); // 关闭老的标准输出
dup(to_par[1]); // 将管道的写复制到标准输出
close(to_par[1]); // 关闭不必要的管道描述符
close(to_chil[0]);
close(to_par[0]);
close(to_chil[1]);
for (;;) {
if ((count = read(0, buf, sizeof(buf)) == 0)
exit();
write(1, buf, count);
}
}

// 父进程在此执行
close(1); // 重新设置标准输入、输出
dup(to_chil[1]);
close(0);
dup(to_par[0]);
close(to_chil[1]);
close(to_par[0]);
close(to_chil[0]);
close(to_par[1]);
for (i = 0; i < 15; i++) {
write(1, string, strlen(string));
read(0, buf, sizeof(buf));
}
}

子进程从父进程继承了文件描述符0和1(标准输入和标准输出)。两次执行系统调用pipe 分别在数组to_par 和to_chil 中分配了两个文件描述符。然后该进程执行系统调用fork,并复制进程上下文:象前一个例子一样,每个进程存取自己的私有数据。父进程关闭他的标准输出文件(文件描述符1),并复制(dup)从管道线to_chil 返回的写文件描述符。因为在父进程文件描述符表中的第一个空槽是刚刚 由关闭腾出来的,所以核心将管道线写文件描述符复制到了文件描述符表中的第一项中,这样,标准输出文件描述符变成了管道线to_chil 的写文件描述符。父进程以类似的操作将标准输入文件描述符替换为管道线to_par 的读文件描述符。与此类似,子进程关闭他的标准输入文件(文件描述符0),然后复制 (dup) 管道线to_chil 的读文件描述符。由于文件描述符表的第一个空项是原先的标准输入项,所以子进程的标准输入变成了管道线to_chil 的读文件描述符。子进程做一组类似的操作使他的标准输出变成管道线to_par 的写文件描述符。然后两个进程关闭从pipe 返回的文件描述符。上述操作的结果是:当父进程向标准输出写东西的时候,他实际上是写向to_chil--向子进程发送数据,而子进程则从他的标准输入读管道线。当子进程向他的标准输出写的时候,他实际上是写入 to_par--向父进程发送数据,而父进程则从他的标准输入接收来自管道线的数据。两个进程通过两条管道线交换消息。

无论两个进程执行的顺序如何,这个程序执行的结果是不变的。他们可能去执行睡眠和唤醒来等待对方。父进程在15次循环后退出。然后子进程因管道线没有写进程而读到“文件尾”标志,并退出。

(http://docs.huihoo.com/gnu/linux/fork.html )

 

对fork函数的体会  进程的创建 (http://blog.csdn.net/binghuiliang/archive/2007/10/22/1836710.aspx)
创建一个进程的系统调用很简单.我们只要调用fork函数就可以了。

#include <unistd.h>

pid_t  fork();
    当一个进程调用了fork以后,系统会创建一个子进程。这个子进程和父进程不同的地方只有他的进程ID和父进程ID,其他的都是一样。就象符进程克隆(clone)自己一样。当然创建两个一模一样的进程是没有意义的。为了区分父进程和子进程,我们必须跟踪fork的返回值。 当fork掉用失败的时候(内存不足或者是用户的最大进程数已到)fork返回-1,否则fork的返回值有重要的作用。对于父进程fork返回子进程的ID,而对于子进程,fork返回0。我们就是根据这个返回值来区分父子进程的。父进程为什么要创建子进程呢?前面我们已经说过了Linux是一个多用户操作系统,在同一时间会有许多的用户在争夺系统的资源。有时进程为了早一点完成任务就创建子进程来争夺资源。 一旦子进程被创建,父子进程一起从fork处继续执行,相互竞争系统的资源。有时候我们希望子进程继续执行,而父进程阻塞,直到子进程完成任务。这个时候我们可以调用wait或者waitpid系统调用。

 

fork函数学习:(http://blog.csdn.net/saturnbj/archive/2009/06/19/4282639.aspx)

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

main ()

{

  pid_t pid; pid=fork();

  if (pid < 0)

    printf("error in fork!");

  else if (pid == 0)

    printf("i am the child process, my process id is %dn",getpid());

  else

    printf("i am the parent process, my process id is %dn",getpid());

}

这段代码写了一个使用fork函数创建子进程,父子进程同时运行而产生交错的,不一样的运行结果。运行结果如下:

# ./a.out

i am the child process, my process id is 4286

i am the parent process, my process id is 4285

fork在英文中是叉子,分叉的意思,在函数fork中,取后面的意思。很形象的表示程序从这里分叉,fork函数创建了子进程,子进程和父进程同时(其实是cpu分时处理)开始运行分叉之后的程序。

程序改写下:

main()

{

  pid_t pid;

  printf("/n[%d]not fork pid=%d/n",getpid(),pid);

  pid=fork();

  printf("/n[%d]forked pid=%d/n",getpid(),pid);

  if(pid<0)

  {

    printf("error in fork!/n");

    getchar();

    exit(1);

  }

  else if(pid==0)

    printf("/n[%d]in child process,p_id=%d/n",getpid(),getpid());

  else

  {

    printf("/n[%d]in parent process,my pid=%d/n",getpid(),pid);

    printf("/n[%d]in parent process,my getpid=%d/n",getpid(),getpid());

  }

}

程序运行结果如下:

$ ./fork

[3819]not fork

[3820]forked pid=0

[3820]in child process,p_id=3820

[3819]forked pid=3820

[3819]in parent process,my pid=3820

[3819]in parent process,my getpid=3819

可以清楚的看到 not fork只打印了一次,其中[3819]是父进程的进程号,创建fork以后,fork函数返回给父进程的值pid是子进程的进程号[3820],而在子进程中,pid值为零。也就是说子进程中,pid被置零。 引用网上一位网友的解释“其实就相当于链表,进程形成了链表,父进程pid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其pid为0. ”

一个很有意思的程序:

int main()

{

  int i;

  for( i= 0; i< 3; i++)

  {

    int pid= fork();

    if(pid== 0)

    {

      printf("son/n");

    }

    else

    {

       printf("father/n");

    }

  }

return 0;

}

大家想想看最后将出现几个son 几个father呢?。。。。。。。

对一下答案吧:

$ ./fork

father

son

son

son

father

father

son

father

son

son

father

father

son

father

总共7个son7个father。你答对了么? 这道题需要在纸上画画才好理解 for i=0 1 2 father father father son son father son son father father son son father son 其中每一行分别代表一个进程的运行打印结果。当产生子进程的时刻,子进程打印son,当子进程调用fork的生成子子进程,他就提升为father。总结来说,father永远打印father,son在fork之前是son,fork之后就为father,同时生成新的son。

原创粉丝点击