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包括变量,缓冲区,内存。另外,父子进程共享同一文件表,当在一个进程中关闭文件时,在另一个进程中仍然是有效的,相应的文件表不会被释放。
本文详细介绍了Linux中的fork函数,阐述了进程的概念,包括进程的上下文交换和组成元素。重点讲解了fork函数创建子进程的过程,强调了父子进程在数据空间、文件描述符共享方面的特性。通过示例代码展示了fork函数的使用,解释了不同情况下printf的输出行为,并分析了父子进程关闭文件描述符的影响。

570

被折叠的 条评论
为什么被折叠?



