linux(unix)进程与文件的关系错综复杂,本教程试图详细的阐述这个问题。
包括:
1、linux多/单进程与多/单文件对于文件流和描述符在使用时的关联情况及一些需要注意的问题。
2、fork,vfork流缓冲等对文件操作的影响。
1、linux文件系统结构
首先补充一点基础知识,了解一下linux文件系统。如下图所示:
图1 磁盘,分区和文件系统
应该明白,上图所示结构是硬盘中文件存放方式的一种逻辑表现形式,与进程无关。对于其中一些术语,见下面的解释。
i节点:包含文件/目录的几乎全部-适用于放置在硬盘上的,需要长久保存的信息。
例如:文件所有者,文件类型,i节点号(存放在目录块中),主次设备号,连接计数,访问/修改时间,IO块长,文件字节数等等。
可以用stat函数(#include <sys/stat.h>)获取相关i节点信息信息。
2、简单的进程与文件关系
下面,我们了解一下单进程多文件以及多进程单文件间的关系,在不考虑fork(父子进程)的情况下,除了赋值语句给我们带来一些小麻烦以外,这个问题还是相当容易的。
2.1、一个进程同时打开多个文件:
图2 一个进程同时打开2个不同文件时内核数据结构
其中,v节点我们几乎已经介绍过了,因为它除了包含i节点之外,自身的内容实在是不怎么多,重点看一下文件表吧。
对于文件表,要注意,它并不是从在于硬盘中的东西,可以说,他是进程的一部分(可能是由操作系统内核负责维护,本人未考证,因为它到底是进程的还是内核的,对于我们要探讨的问题无关紧要)。文件表包括:
文件状态标志:包含读,写,添写,同步和非阻塞等各种文件打开/当前状态。
V节点:文件类型和对此文件进行各种操作的函数的指针,这些信息都是在打开文件时候由磁盘读入内存的。
I节点:同上节所述。
可用fcntl函数(#include <fcntl.h>)修改文件表内容。
2.2、多个无关联进程同时打开一个文件:
图3 两个进程同时打开同一个文件时内核数据结构
此时,2个文件分别使用不同的文件表,这说明不同进程间文件状态标志,文件偏移量等都是独立的。但他们共用同一个v节点表。对于这种结构的特性,理解起来因该是个轻松的事情。
3、文件描述符或流的复制
对于文件描述符或流的复制,很多情况我们会采用赋值语句,下面了解一个赋值和dup的不同之处,dup函数复制文件描述符后的内核数据结构:
图4 执行dup函数(#include <unistd.h>)复制文件描述符后,内核数据结构。
此时,2个fd文件标志同时使用同一文件表。
3.1、dup与赋值语句用于文件描述符的区别
为了了解dup与赋值语句用于文件描述符的区别,请看如下程序。
程序描述:
打开一个文件描述符,分别适用dup和赋值语句进行复制,复制之后,打印原始和被复制的文件描述符id,看看是否具有相同的值,然后关闭文件,测试关闭是否成功。
程序示例:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int sys_err(char *str)
{
puts(str);
exit(0);
}
int main(void)
{
int p,q;
if((p=open("c_fid.c", O_RDONLY)) == -1)
sys_err("open error");
q = dup(p);
puts("dup:");
printf("file p,q fd is:%d %d/n", q, p);
printf("close file p ok?: %d/n", close(p));
printf("close file q ok?: %d/n", close(q));
if((p=open("c_fid.c", O_RDONLY)) == -1)
sys_err("open error");
q = p;
puts("=:");
printf("file p,q fd is:%d %d/n", q, p);
printf("close file p ok?: %d/n", close(p));
printf("close file q ok?: %d/n", close(q));
return 0;
}
程序运行结果:
dup:
file p,q fd is:4 3 //文件p,q使用不同的文件描述符
close file p ok?: 0
close file q ok?: 0 //文件关闭成功
=:
file p,q fd is:3 3 //简单复制
close file p ok?: 0
close file q ok?: -1//关闭失败,原因是此描述符已经被关闭了
由此证明,dup是产生一个新的文件描述符id和指针,但是他们共用文件表,效果如图4,这时,关闭一个文件描述符,另外一个仍旧可用,文件表并不会被释放。而赋值语句不同,它只是简单的在另外一个变量中记录原始文件指针等,2个变量的文件描述符相同,进程表项中并不产生新的项目。
3.2、赋值语句复制标准流。
例:
#include <stdio.h>
#include <stdlib.h>
int sys_err(char *str)
{
puts(str);
exit(0);
}
int main(void)
{
FILE *p,*q;
if((p=fopen("c_fid.c", "r")) == NULL)
sys_err("open error");
q = p;
printf("FILE p,q fd is:%d %d/n", fileno(q),fileno(p));
printf("close file p ok?: %d/n", fclose(p));
printf("close file q ok?: %d/n", fclose(q));
return 0;
}
程序执行结果:
FILE p,q fd is:3 3 //2个流共用同一个文件描述符
close file p ok?: 0
*** glibc detected ***//2次关闭引起错误,造成程序崩溃。
…………
4、 引入fork后进程与文件关系以及流缓冲对文件操作的影响
4.1 fork
图5 使用fork之后,父进程、子进程之间对打开文件的共享
使用fork后,子进程会继承父进程所打开的文件表,此时,父子进程使用同一个文件表(可见,上面猜测文件表由内核维护因该是正确的,因为文件表并没有被复制),这说明2个进程将共用文件状态,文件偏移量等信息。如果使用close关闭一个进程中的文件描述符,那么另一个进程中,此描述符仍然有效,相应文件表也不会被释放。
需要注意的是,在使用c标准io进行文件读写时,先结束的进程会将缓冲区内数据也计入文件偏移量的长度,对于相应文件缓冲区类型和长度,可以使用setbuf,setvbuf(#include <stdio.h>)设置。
程序示例:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
int main(void)
{
int pid;
FILE *p;
char buff[20]={0};
int test=-2;
if((p=fopen("/media/lin_space/soft/netbeans-6.5-ml-cpp-linux.sh", "r")) == NULL)
{//文件大于4096字节
puts("open error.");exit(0);
}
if((pid=fork()) < 0)
{
puts("fork error");
exit(0);
}
else if(pid == 0)
{
sleep(2);
test = ftell(p);//返回当前偏移量
printf("/nchild - ftell is: %d/n", test);
if((buff[0] = fgetc(p)) != EOF)
printf("child - fgetc is: %c/n", buff[0]);
else
puts("child - fgetc error/n");
test = ftell(p);
printf("child - ftell is: %d/n", test);
}
else
{
test = ftell(p);
printf("/nparent - ftell is: %d/n", test);
if((buff[0] = fgetc(p)) != EOF)
printf("parent - fgetc is: %c/n", buff[0]);
else
puts("parent - fgetc error/n");
test = ftell(p);
printf("parent - ftell is: %d/n", test);
}
printf("parent and child - close file ok?: %d/n", fclose(p));
return 0;
}
程序执行结果:
parent - ftell is: 0
parent - fgetc is: #
parent - ftell is: 1
parent and child - close file ok?: 0
freec@freec-laptop:/media/lin_space/summa/apue/unit8$ //父进程结束
child - ftell is: 4096 //子进程文件偏移量为4096,原因是文件缓冲类型为全缓冲,缓冲区大小为4096
child - fgetc is: a
child - ftell is: 4097
parent and child - close file ok?: 0 //文件关闭成功
4.2 vfork
而对于vfork,虽然子进程运行于父进程的空间,但是子进程却拥有自己的进程表项(包含进程pid,fd标志,文件指针等),所以,在其中一个进程结束后,另外一个进程的文件描述符依旧有效,子进程得到的是父进程文件描述符的副本。
但是vfork对于标准流,情况则不同,一个进程结束后(如果不是使用_exit()结束进程),则此进程结束时可能会冲洗流缓冲区,并且关闭流,对于是否这样做,要取决于具体实现