fork(2), stdio 行/块缓冲区,子进程为何输出fork()之前的东西

本文探讨了在使用fork创建子进程时,stdio缓冲区导致printf输出两次的问题。原因在于缓冲区在子进程中被复制,解决办法包括使用fflush刷新缓冲区或_setbuf设置无缓冲。同时,write输出不受影响,因其直接写入内核缓冲区。建议在混合使用stdio函数和系统调用时谨慎处理缓冲区。
/****************************
        > File Name: fork_stdio.c
      > Author: dulun
      > Mail: dulun@xiyoulinux.org
      > Created Time: 2016年07月26日 星期二 09时22分08秒
 **********************/

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

int main()
{
    printf(" %d hello world\n", getpid());
    write(STDOUT_FILENO, "Ciao\n", 5);

    if( -1 == fork())
    {
        exit(1);
    }

    return 0;
}

这里写图片描述

如上代码,生成输出结果乍看令人费解。

以上输出有两件怪事: printf()的结果输出行,输出了两次,且write()先于printf输出。

为什么printf()输出了两次???

首先要记住,是在进程的用户空间内存中,维护stdio缓冲区的.因此,通过fork()创建的子进程会复制这些缓冲区,当标准输出定向到终端时,因为缺省为行缓冲,所以会理解显示printf输出的包含换行符的字符串。不过,当标准输出重定向到文件时,由于缺省块缓冲,所以在上述代码中,调用fork()时,printf()输出的字符串仍然在父进程的缓冲区中,并随子进程的创建而产生一份副本。父,子进程调用exit()时会刷新各自stdio缓冲区,从而导致重复输出的结果。

如何解决:

(1)针对stdio缓冲区问题的特定解决方案,可以在调用fork()之前使用函数fflush()来刷新stdio缓冲区,或使用setbuf(stdio, NULL)和setvbuf(stdio, NULL, _IONBF(无缓冲), 0) 来关闭stdio流的缓冲功能。
(2)子进程可以调用_exit(0)而非exit()以便不再刷新stdio缓冲区。
一个更为通用的原则:在创建子进程的应用中,典型情况下仅有一个进程(一般为父进程)应通过调用exit()终止,而其他进程应调用_exit()终止,从而保证只有一个进程调用退出程序并刷新stdio缓冲区。
有时确实希望在fork()之后刷新缓冲区,这是见机行事即可,要么exit()要么显式调用fflush()之类的。
(3) write()并未输出两次,因为write()会将数据传给内核缓冲区fork()不会复制这一缓冲区。

(4) write()的输出结果先于printf()出现,因为write()会将数据立刻传给高速缓存,而printf()等调用exit()刷新stdio缓冲区时才输出。
通常,在混用stdio函数和系统调用对同一文件进行I/O操作时,需要特别谨慎

实验一:进程管理 一、实验目的 通过学习Linux操作系统的基本操作,了解操作系统中多用户、多任务等相关概念,加深对进程概念的理解,明确进程与程序的区别。进一步认识并发执的实质。掌握进程同步的实现方法,了解Linux系统中进程通信的基本原理。 二、实验内容 1. 用户子进程的创建fork( )函数 功能:用户可以在自己的进程中创建多个子进程,以实现多个不同任务并发执。Linux提供的创建子进程的系统调用是int fork( );对函数的返回值进判断,①若为0,则创建成功,从子进程返回;②若大于0,则创建成功,从父进程返回,其值为子进程pid号;③若为-1,则创建失败。 例1 父进程创建子进程P1,P2,父子进程分别输出字符a、b和c,程序清单如下所示,设文件名为file1.c。 #include <stdio.h> // putchar()、perror() #include <unistd.h> // fork()、getpid()、getppid() #include <sys/wait.h> // waitpid()(回收子进程资源) #include <sys/types.h> // pid_t 类型定义 int main() { pid_t P1, P2; // 用 pid_t 存储进程ID,符合规范 while ((P1 = fork()) == -1) { perror("fork P1 failed"); // 输出失败原因,便于调试 sleep(1); // 失败后休眠1秒再重试,避免频繁占用CPU } if (P1 == 0) { // 子进程P1:输出'b',附带PID信息(便于验证) printf("子进程P1 [PID:%d] 输出:b\n", getpid()); } else { // 父进程:创建第二个子进程P2 while ((P2 = fork()) == -1) { perror("fork P2 failed"); sleep(1); } if (P2 == 0) { // 子进程P2输出'c',附带PID信息 printf("子进程P2 [PID:%d] 输出:c\n", getpid()); } else { // 父进程:输出'a',附带PID和子进程PID信息 printf("父进程 [PID:%d] 输出:a(子P1:%d,子P2:%d)\n", getpid(), P1, P2); // 回收两个子进程资源,避免僵尸进程 waitpid(P1, NULL, 0); // 等待子进程P1退出 waitpid(P2, NULL, 0); // 等待子进程P2退出 printf("父进程:已回收所有子进程资源\n"); } } return 0; } 分析:该程序编译连接并连续运多次后,输出的结果是随机的,如abc,bca,bac,cab等,这是因为它们执的先后次序不受程序源代码中分支顺序的影响,只要父子进程之间没有使用同步的工具来控制其执序列,则父子进程并发执的顺序取决于操作系统的调度。该例中,父子进程的映像如图9.7所示。 图9.7 父子进程的映像 例2 父进程创建子进程,两个进程并发执,分别输出各自的字符串。程序清单如下所示,设文件名为file2.c。 #include<sys/types.h> #include<unistd.h> main( ){ pid_t pid; pid=fork( ); if(!pid) printf(“this is child”); else if (pid>0) printf(“this is parent,child has pid %d\n”,pid); else printf(fork fail”); } 分析:本例将上例中的每个进程的输出由单个字符改为一个字符串,由于使用printf()函数,则输出的字符串不会被中断,因此,字符串内部字符的输出顺序是不变的。但由于父子进程的并发执,各自所输出的整体字符串顺序会随机发生变化,这与打印单字符的结果是相同的。 例3 连续2fork( )的进程家族树,程序清单如下,设文件名为file3.c #include <stdio.h> main() { fork( ); //父进程1创建子进程2 fork( ); //父进程1返回后创建子进程3,子进程2返回后创建其子进程4 putchar(‘A’); } 分析:具体过程是父进程创建了子进程,当父、子进程返回时又分别创建了两个子进程,故运输出4个“A”字符,这表现程序运后一共有4个进程。其进程家族树如图9.8所示。 图9.8 进程家族树 一般地,如果连续使用n个fork( )系统调用,而不用if…else结构对返回值进判断以区分父子进程的空间,则n个fork( )执后所创建的进程家族树中的进程总数为2n个。 2.获取当前进程的进程标识符getpid( )函数 功能:getpid( )函数用来获取父进程标识符,getpriotity(void)用来获取进程,进程组和用户进程的执优先级。 例4 输出父、子进程各自的内部标识符及其父进程的内部标识符以及fork()函数的返回值。 int main(void) { int i = 0; printf("i chi/par ppid pid fpid\n"); for (i = 0; i < 2; i++) { pid_t fpid = fork(); if (fpid == 0) { printf("%d child %4d %4d %4d\n", i, getppid(), getpid(), fpid); // 子进程完当前逻辑后退出,避免继续参与循环(否则会创建更多子进程) return 0; } else if (fpid > 0) { // 父进程输出,补充\n刷新缓冲 printf("%d parent %4d %4d %4d\n", i, getppid(), getpid(), fpid); waitpid(fpid, NULL, 0); } else { // 补充:fork()失败处理(原代码缺失) perror("fork failed"); return 1; } } return 0; } 分析:本程序运结果共输出7信息,第1为提示信息,第2、3输出i=0时父、子进程的相应信息,这里需要注意的是:fork()函数被调用一次,却有两个返回值,若是子进程,则返回是0;若是父进程,则返回大于0的值并且该值是子进程的内部标识符。 这里,ppid()为进程的父进程标识符。同理,当i=1时,又输出了第4至第7相应信息。这因为原父子进程又各自创建两个子进程,注意分析getppid( )、getpid( )函数值得变化。 三、实验环境及过程 Linux环境下,用gedit或vi编辑器将上述4个程序编辑、保存(命令:gedit 或vi 文件名.c),利用GCC编译器编译(命令:gcc [-o文件名] 文件名.c),运程序(命令./a.out或./文件名)./a.out。 四、演示执结果并截图写到实验报告中 五、分析程序执,进一步理解进程及其并发的概念
11-21
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值