关于fork和缓冲区的问题

来源:互联网 发布:linux 修改profile 编辑:程序博客网 时间:2024/04/30 16:58

题目:请问下面的程序一共输出多少个“-”?


    #include <stdio.h>      #include <sys/types.h>      #include <unistd.h>             int main(void)      {         int i;         for(i=0; i<2; i++){            fork();            printf("-");         }                return 0;      }  


一般熟悉fork机制会认为输出为6个'-',但我们运行一下,输出的是8个'-'.

基础知识见上一个题.

>>>程序一开使,bash产生一个进程P1执行此程序,

>>>P1进入程序后,当i=0时,fork()产生一个子进程P2,同时它自己输出一个'-'.

>>>P2继承P1的诸如环境变量,PC等环境,P2首现会输出一个'-'.同时此时i=1,会继续执行for循环---P2先fork()出一个子进程P3,同时再输出一个'-'.

>>>P3进程为P2的子进程,它会复制其父进程P2的指令,变量值,程序调用栈,环境变量,缓冲区等,它会输出一个'-'

       因为这里P3会继承P2的缓冲区,其中有一个'-',所以P3会输出两个'-'.

>>>P1进入程序后,当i=1时,fork()产生另一个它的子进程P4,同时输出一个'-'.

>>>P4同样会输出一个'-'.

       因为P4为P1的一个子进程它会继承P1的缓冲区,其中有一个'-',所以P4会输出两个'-'.

我们正常如上分析觉得应该产生6个'-'.

但为什么是8个呢,見红色部分加上的分析.

这是因为printf(“-”);语句有buffer,所以,对于上述程序,printf(“-”);把“-”放到了缓存中,在fork的时候,缓存被复制到了子进程空间,所以,就多了两个,就成了8个,而不是6个。

如果把上面的printf("-")改为

    printf("-\n");  


或着调用fflush清一下缓存.

    printf("-");      fflush(stdout);  


都可以保正输出6个'-'





1. 进程的概念

进程可以理解为程序的一次执行过程。每一个特定的时候只有一个进程占用CPU。

当一个进程的时间片用完后,系统把相关的寄存器的值保存到该进程表相应的表项里。同时把代替该进程即将执行的进程的上下文从进程表中读出,并更新相应的寄存器值,这个过程称为上下文交换。

上下文交换其实就是读出新的进程的PC(程序计数器),指示当前进程的下一条将要执行的指令。

 

一个进程主要包含以下三个元素:

(1)一个正在执行的程序

(2)与该进程相关联的全部数据(变量,内存,缓冲区)

(3)程序上下文(程序计数器)

 

2. pid=fork()的创建过程

 

fork()函数是用来创建子进程的,当一个程序创建了一个子进程,那么原先的进程称为该子进程的父进程。操作系统的进程表的每个表项中存放着一个进程的情况。首先,操作系统在进程表中为该进程建立新的表项。子进程与父进程共享代码段,但数据空间是相互独立的。子进程数据空间的内容是父进程的完整拷贝,上下文也完全相同。pid在父进程与子进程的返回值是不同的,如果pid<0创建失败。在子进程中返回的pid=0,因为子进程可以通过getpid()得到自己的进程ID。在父进程中返回的是子进程的实际的PID。子进程是从fork()之后执行的,此时,父进程与子进程就fork(分道扬镳)了。

 

注意:

(1)子进程copy父进程的变量,内存与缓冲区,即整个的数据空间的内容,但数据空间是独立的。

(2)父子进程对打开文件的共享: fork之后,子进程会继承父进程所打开的文件表,即父子进程共享文件表,该文件表是由内核维护的,两个进程共享文件状态,偏移量等。这一点很重要,当在父进程中关闭文件时,子进程的文件描述符仍然有用,相应的文件表也不会被释放。

 

3. fork()函数例子

 


/**
进程的基本操作fork()系统调用
fork()是由父进程创建子进程,并且把父进程的数据结构即上下文复制给子进程
对父进程而言,返回的是子进程的进程ID,即PID,对子进程而言,返回的是0
父进程调用wait函数时阻塞,直到子进程进入僵死状态,这时子进程的退出可通过wait函数返回给父进程,wait常用来判断子进程是否结束
fork()进程的执行过程
一个进程包含三个部分:
(1)正在执行的程序
(2)和该进程相关联的全部数据(内存,缓冲区,变量)
(3)程序上下文
当父进程fork()之后,创建子进程,为子进程建立一个新的表项,关于子进程的全部信息就保存在这个表项里面了
父进程与子进程共享代码段,但两者的数据段与堆栈段是分区开,子进程数据段的空间内容是从父进程复制而来的
而程序上下文指的是程序计数器PC的位置,即进程执行到了的位置。而在fork()之后就分开执行了,在fork之前,可以看成是两个独立的程序,拥有共同的代码段
在子进程中返回的PID为0,在父进程返回的是子进程的PID,因为在子进程中通过getpid()可以得到子进程的PID.
总之,子进程可以看成是与父进程是并列的两个程序,在fork之后分开执行的。共享代码段,但数据段与堆栈段是分开的,内容是从父进程复制而来的
在C语言中,有以下几种情况会刷新缓冲区:
(1)缓冲区满
(2)遇到换行符/n时
(3)第三种调用fflush函数时
(4)程序正常退出,异常退出就不保证了
当printf在fork函数之前,首先父进程执行printf,此时并不输出内容,由于没有/n 。当子进程被fork()之后,子进程的执行起点为fork之后,所以子进程并没有执行printf.但由于子进程复制父进程数据空间,缓冲区的内容,所以父进程的缓冲区被子进程复制了。到这里,父进程继续执行没执行完的部分,子进程执行fork()之后的内容,到程序正常结束后,就是上面所说的第(4)条,首先父进程flush自己的缓冲区,然后子进程flush缓冲区。至于谁先谁后flush决定于操作系统,这样就会打印出两行AAAA了。
总之,printf的确是只执行了一次,输出在最后不一定是/n,有可能是上面几种情况之一。

而printf("AAAA/n")则直接刷新缓冲区,输出到终端上。
当父子进程分别打开文件表时,共用文件表的偏移量,状态等信息
因此,在子进程中关闭文件时,父进程照样能用,相应的文件表也不会被释放

**/

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

int main(){
pid_t childpid;//用来保存子进程的PID
char buf[100]={0};//定义缓冲区
int status;
int f;//定义文件标识符
f=open("text",O_CREAT|O_RDWR,0664);
if(f==-1){//文件标识符
perror("文件创建失败");
return 1;
}
strcpy(buf,"父进程数据");
printf("%d/n",strlen(buf));
//此时创建子进程
childpid=fork();
printf("childpid=%d",childpid);
if(childpid==0){
strcpy(buf,"子进程数据");
printf("%s/n",buf);
puts("子进程正在工作");
printf("子进程的PID是%d/n",getpid());
printf("父进程的PID是%d/n",getppid());
int n1= write(f,buf,strlen(buf));//把buf的数据输出重定向到文件中去,strlen是返回字符串的长度,而sizeof返回的是类型所占空间的大小,前面是个函数,后面是一个操作符,sizeof是编译时求值,而strlen是运行时求值
strcpy(buf,"子进程数据");
write(f,buf,strlen(buf));
close(f);
exit(0);//子进程调用exit函数进入僵死状态,参数0表示正常退出
}

else {
puts("父进程正在工作");
printf("%s/n",buf);
printf("父进程的PID是%d/n",getpid());
printf("子进程的PID是%d/n",childpid);
int n=write(f,buf,strlen(buf));
close(f);
}

wait(&status);//wait函数是一个等待子进程退出的函数,其参数是一个int类型的指针,保存子进程退出的一些信息

return 0;
}

程序运行结果如下:

15   //父进程打印出来的
childpid=0子进程数据
子进程正在工作
子进程的PID是7271
父进程的PID是7270
childpid=7271父进程正在工作
父进程数据
父进程的PID是7270
子进程的PID是7271

如果把 printf("%d/n",strlen(buf)); 改为printf("%d",strlen(buf)); ,那么运行的结果为:

 

15childpid=0子进程数据  //这个15是子进程打印出来的
子进程正在工作
子进程的PID是7284
父进程的PID是7283
15childpid=7284父进程正在工作
父进程数据
父进程的PID是7283
子进程的PID是7284

 

分析一下:

1. wait()函数阻塞父进程,直到子进程返回。因此,子进程先执行,直到退出为止。

为什么printf("%d/n",strlen(buf)); 与 printf("%d",strlen(buf));打印的结果不相同?

printf("%d/n",strlen(buf)); 与 printf("%d",strlen(buf)); 区别:

前者将数据已经输出到终端上了,后者的数据还在缓冲区内。

当创建子进程时,子进程要copy父进程的数据,包括copy缓冲区,所以,第一个程序只打印出一个15,而第二个程序打印出两个15.

还要注意一点,第一个结果的15是由父进程打印出来的,而第二个结果由于子进程先执行,复制缓冲区,所以子进程先打印出15,而后父进程才打印出15.

 

2. close(f),当子进程已经关闭了文件,父进程怎么还能将数据写入?

 在前面的分析中得知,父子进程共享同一个文件表,共享文件表的状态,偏移位置等信息。所以在子进程关闭文件描述符后,在父进程中仍然是有效的,而父进程写数据也从文件的当前位置开始写。

 

而linux系统文件流,缓冲及文件描述符与进程之间的关系,可参考http://topic.csdn.net/u/20090309/18/3aba9e11-c8a8-492b-9fe7-29043974a102.html

 

总之,父子进程是共享代码段的,但数据空间是分开的,而子进程数据空间的内容是来自父进程的copy,这些copy包括变量,缓冲区,内存。另外,父子进程共享同一文件表,当在一个进程中关闭文件时,在另一个进程中仍然是有效的,相应的文件表不会被释放。


0 0
原创粉丝点击