对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描叙符。当读、写一个文件时,用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。
按照惯例,UNIX shell使文件描述符0与进程的标准输入相结合,文件描述符1与标准输出相结合,文件描述符2与标准出错输出相结合。这是unix shell以及很多应用程序使用的惯例,而与内核无关。尽管如此,如果不遵循这种惯例,那么很多unix应用程序不能工作 。
在POSIX. 1应用程序中,幻树0,1,2应该被代换成符号常数STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO。这些常数都定义在头文件中。
早期的unix版本采用的上限值是19(允许每个进程打开20个文件),现在很多系统则将其增加至63。
二、open函数
调用open可以打开或者创建一个文件
运行结果:
A.open()和creat()调用成功返回文件描述符,失败返回-1,并设置errno。
B.open()/creat()调用返回的文件描述符一定是最小的未用描述符数字。
C.creat()等价于open(pathname,O_CREAT | O_WRONLY | O_TRUNC,mode)
D.open()可以打开设备文件,但是不能创建设备文件,设备文件必须使用mknod()创建。
从以上我们可以看到open有两个造型,一个是两个参数,一个是三个参数。
注意:对于open函数而言,仅当创建新文件时才使用第三个参数。
pathname是要打开或创建文件的名字
flags参数可用来说明此函数的多个选择项,用下列一个或多个常数进行组合
A.O_RDONLY 只读打开
B.O_WRONLY 只写打开
C.O_RDWR 读、写打开
D.O_APPEND 每次写时都加到文件的尾端
E.O_CREAT 此文件不存在则创建它。使用此选择项时,需同时说明第三个参数mode,用其说明该新文件的存取许可权位。
F.O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则出错。这可测试一个文件是否存在,如果不存在则创建此文件成为一个原子操作。
G.O_TRUNC 如果此文件存在,而且只为读或只写成功打开,则将其长度截断为0。
H.O_NOCTTY 如果pathname指的是终端设备,则不将此设备分配作为此进程的控制终端。
I.O_NONBLOCK 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I/O操作设置非阻塞方式。
思考:fopen的参数mode与open的参数 flags之间的等价关系?
r -------->O_RDONLY
r+------->O_RDWR
w-------->O_WRONLY | O_CREAT | O_TRUNC,0666
w+------>O_RDWR | O_CREAT | O_TRUNC,0666
a--------->O_WRONLY | O_CREAT | O_APPEND,0666
a+-------->O_RDWR | O_CREAT | O_APPEND,0666
注意:当创建一个文件名长度超过NAME_MAX(ubuntu上默认为255)的文件时,Linux总是返回错误。
案例一、
- #include <stdio.h>
- #include <sys/types.h>
- #include <fcntl.h>
- #include <string.h>
- #include <errno.h>
- int main(int argc,char *argv[])
- {
- FILE *fp;
- int fd;
- int ch;
- if(argc < 2)
- {
- fprintf(stderr,"usage : %s argv[1]\n",argv[0]);
- return -1;
- }
-
- if((fd = open(argv[1],O_WRONLY | O_CREAT,0777)) < 0)
- {
- fprintf(stderr,"Fail to open %s : %s\n",argv[1],strerror(errno));
- return -1;
- }
- return 0;
- }
cyg文件原本不存在,运行程序后,创建了cyg文件,可以看到cyg文件的实际权限和指定的权限不一样。
注意:文件的实际权限 = mode & ~umask,umask系统默认值为0022,可以通过/etc/profile文件进行修改
rwxr-xr-x = 0777 & ~0022 = 755
案例 二
运行结果:
案例三、
运行结果如下:
我们打开的时候,指定的操作权限是只写,所以我们就行读操作就失败了。可以看出,打开文件时候指定的操作权限,限制了后期对文件的操作权限。
三、read函数案例 二
- #include <stdio.h>
- #include <sys/types.h>
- #include <fcntl.h>
- #include <string.h>
- #include <errno.h>
- int main(int argc,char *argv[])
- {
- FILE *fp;
- int fd;
- int ch;
- if(argc < 2)
- {
- fprintf(stderr,"usage : %s argv[1]\n",argv[0]);
- return -1;
- }
-
- if((fd = open(argv[1],O_WRONLY | O_CREAT,0777)) < 0)
- {
- fprintf(stderr,"Fail to open %s : %s\n",argv[1],strerror(errno));
- return -1;
- }
-
- if((fp = fdopen(fd,"r")) == NULL)
- {
- perror("Fail to fdopen");
- return -1;
- }
-
- return 0;
- }
fdopen函数的功能是通过以打开的文件描述符,获取对应的流指针,这样就可以通过标准I/O的库函数来操作此文件。
以上我们在操作的过程中,open打开文件是以只写形式打开,而在fdopen的时候,指定的权限是只读,导致调用fdopen函数失败
注意:fdopen在指定权限时,应该和打开文件时的操作权限一样
案例三、
- #include <sys/types.h>
- #include <fcntl.h>
- #include <string.h>
- #include <errno.h>
- int main(int argc,char *argv[])
- {
- FILE *fp;
- int fd;
- int ch;
- if(argc < 2)
- {
- fprintf(stderr,"usage : %s argv[1]\n",argv[0]);
- return -1;
- }
-
- if((fd = open(argv[1],O_WRONLY | O_CREAT,0744)) < 0)
- {
- fprintf(stderr,"Fail to open %s : %s\n",argv[1],strerror(errno));
- return -1;
- }
- if((fp = fdopen(fd,"w")) == NULL)
- {
- perror("Fail to fdopen");
- return -1;
- }
- if((ch = fgetc(fp)) == EOF)
- {
- perror("Fail to fgetc");
- return -1;
- }
- return 0;
- }
我们打开的时候,指定的操作权限是只写,所以我们就行读操作就失败了。可以看出,打开文件时候指定的操作权限,限制了后期对文件的操作权限。
函数功能:从打开的文件中读取数据
返回值 : 成功,读到的字节数; 0,已到达文件尾;-1 ,出错.
fd : 文件描述符
buf : 从指定存储器读出数据存放的位置
count : 指定读出的字节数
一下情况会导致读到的子节数小于count
A.读取普通文件时,读到文件末尾还不够count字节。例如:如果文件只有30字节,而我们想读取100字节,那么实际读到的只有30字节,read函数返回30。此时再使用read函数作用于这个文件会导致read返回0。
B.从终端设备(treminal device)读取时,一般情况下每次只能读取一行。
C.从网络读取时,网络缓存可能导致读取的字节数小于count字节
D.读取pipe或者FIFO时,pipe或FIFO里的字节数可能小于count
E.从面向记录(recond-oriented)的设备读取时,某些面向记录的设备(如磁带)每次最多只能返回一个记录.
F.在读取部分数据时被信号中断
注意:读操作从当前偏移量开始,在成功返回之前,偏移量增加实际读取的字节数.
四、write函数
函数功能 : 向已经打开的文件写数据
返回值 : 成功,已写入的字节数;-1,出错
fd : 文件描述符
buf : 从内存的这个地址开始,向内存写count个字节到文件中。
count : 指定写入的字节数
对于普通文件,写操作始于偏移量。如果打开文件时使用了O_APPEND,则每次写操作都将数据写入文件末尾。成功写入后,偏移量增加,增量为实际写入的字节数。
案例 -- 实现文件拷贝
运行结果:
运行结果如下:
如果两个独立进程各自打开了同一个文件。我们假定第一个进程使该文件在文件描述符3上打开,而另一个进程则使此文件在文件描述符4上打开。打开此文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v结点表项。每个进程都有自己的文件表项的一个理由是:这种安排使每个进程都有它自己的对该文件的当前位偏移量。
案例:
七、dup和dup2函数
运行结果:
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <string.h>
- #include <errno.h>
- #define MAX 100
- int do_copy(int rfd,int wfd)
- {
- char buf[MAX];
- int n;
- while(1)
- {
- if((n = read(rfd,buf,sizeof(buf))) < 0)
- {
- perror("Fail to read");
- return -1;
- }
-
- if(n == 0)
- {
- printf("Read end of file!\n");
- return 0;
- }
- write(wfd,buf,n);
- }
- return 0;
- }
- int main(int argc,char *argv[])
- {
- int rfd,wfd;
- if(argc < 3)
- {
- fprintf(stderr,"usage : %s argv[1] argv[2].\n",argv[0]);
- return -1;
- }
- if((rfd = open(argv[1],O_RDONLY)) < 0)
- {
- fprintf(stderr,"Fail to open %s : %s\n",argv[1],strerror(errno));
- return -1;
- }
- if((wfd = open(argv[2],O_WRONLY | O_CREAT,0666)) < 0)
- {
- fprintf(stderr,"Fail to open %s : %s\n",argv[2],strerror(errno));
- return -1;
- }
- if(do_copy(rfd,wfd) < 0)
- {
- return -1;
- }
- return 0;
- }
五.lseek函数
所有打开的文件都有一个当前文件偏移量(current file offset),以下简称cfo,cfo通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数。读写操作通常开始于cfo,并且使cfo增加所读写的字节数。文件被打开时,cfo会被初始化为0,除非使用了O_APPEND。
返回值:成功,文件的当前位移;-1,出错
fd:文件描述符
offset:偏移量,单位是字节的数量,可正可负(向前、向后移)。
whence:
1.如果whence是SEEK_SET,文件偏移量将被设置为offset。
2.如果whence是SEEK_CUR,文件偏移量被设置为cfo加上offset,offset可以为正也可以为负。
3.如果whence是SEEK_END,文件偏移量将被设置为 文件长度加上offset,offset可以为正也可以为负。
lseek函数只修改文件表项中的文件当前偏移量,没有进行任何的I/O操作
案例--将file.c文件的前一半放在file1.c,后一半放在file2.c
- #include <stdio.h>
- #include <string.h>
- #include <errno.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #define MAX 100
- int half_store(int fd,int fd1,int fd2)
- {
- char ch;
- int len = 0,count = 0,n = 0;
- len = lseek(fd,0,SEEK_END);
- lseek(fd,0,SEEK_SET);
-
- while(1)
- {
- if((n = read(fd,&ch,1)) < 0)
- {
- perror("Fail to read");
- return -1;
- }
-
- if(n == 0)
- {
- printf("Read end of file.\n");
- return 0;
- }
-
- count ++;
- if(count <= len / 2)
- {
- write(fd1,&ch,1);
-
- }else{
-
- write(fd2,&ch,1);
- }
- }
-
- return 0;
- }
- int main(int argc,char *argv[])
- {
- int fd,fd1,fd2;
-
- if(argc < 3)
- {
- fprintf(stderr,"usage : %s argv[1] argv[2].\n",argv[0]);
- return -1;
- }
- if((fd = open(argv[1],O_RDONLY)) < 0)
- {
- fprintf(stderr,"Fail to open %s : %s.\n",argv[1],strerror(errno));
- return -1;
- }
- if((fd1 = open(argv[2],O_WRONLY | O_CREAT,0666)) < 0)
- {
- fprintf(stderr,"Fail to open %s : %s.\n",argv[2],strerror(errno));
- return -1;
- }
- if((fd2 = open(argv[3],O_WRONLY | O_CREAT,0666)) < 0)
- {
- fprintf(stderr,"Fail to open %s : %s.\n",argv[3],strerror(errno));
- return -1;
- }
- if(half_store(fd,fd1,fd2) < 0)
- {
- printf("Fail to half_store.\n");
- return -1;
- }
- return 0;
- }
六、文件共享
UNIX支持在不同进程间共享打开文件。
内核使用三种数据结构表示打开的文件,他们之间的关系决定了多进程的文件共享操作。
A.进程表 : 每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项,与之关联的是文件描述符标志,和一个指向文件表项的指针。
B.文件描述符表:内核为所有打开文件维持一张文件表,每个表项包含文件标志,当前偏移量,指向该文件v结点表项的指针。打开某文件的每个进程得到一个该文件表项。
C.v结点:每个打开的文件都有一个v结点结构。(保存在磁盘里,读入内存的)。Linux中没有v结点的概念,i结点就是v结点,对于一个给定文件,i结点只有一个。
如果两个独立进程各自打开了同一个文件。我们假定第一个进程使该文件在文件描述符3上打开,而另一个进程则使此文件在文件描述符4上打开。打开此文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v结点表项。每个进程都有自己的文件表项的一个理由是:这种安排使每个进程都有它自己的对该文件的当前位偏移量。
在完成每个write后,在文件表项中的当前文件位偏移量即增加所写的字节数。如果当前文件位移量超过了当前文件长度,则在i结点表项中的当前文件长度被设置为当前文件位移量(也就是该文件加长了)。
如果用O_APPEND标志打开了一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有添写标志标志的文件执行写操作时,在文件表项中的当前文件位移量首先被设置为i 节点表项中的文件长度。这就使得每次写的数据都添加到文件的当前尾端处。
lseek函数只修改文件表项中的当前文件位移量,没有进行任何I/O操作。
若一个文件使用lseek被定位到文件当前的尾端,则文件表项中的当前文件位移量被设置为i节点表项中的当前文件长度。
案例:
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <string.h>
- #define MAX 50
- void help()
- {
- printf("CMD 1 : print file pointer position.\n");
- printf("CMD 2 : make file pointer to head of file.\n");
- printf("CMD 3 : make file pointer to end of file.\n");
- printf("CMD 4 : write data to file.\n");
- printf("CMD 5 : read data from file.\n");
- printf("CMD 6 : help info.\n");
- return;
- }
- int function(int fd)
- {
- int cmd;
- char buf[MAX];
- int n;
-
- while(1)
- {
- putchar('\n');
- printf("Input cmd : ");
- if(scanf("%d",&cmd) == 0)
- {
- printf("Quit\n");
- break;
- }
- while(getchar() != '\n');
- switch(cmd)
- {
- case 1:
- putchar('\n');
- printf("CURRENT : %d\n",lseek(fd,0,SEEK_CUR));
- break;
- case 2:
- putchar('\n');
- printf("Start head of file.\n");
- lseek(fd,0,SEEK_SET);
- break;
- case 3:
- putchar('\n');
- printf("Start end of file.\n");
- lseek(fd,0,SEEK_END);
- break;
- case 4:
- putchar('\n');
- printf("Write data to file.\n");
- memset(buf,0,sizeof(buf));
- printf(">");
- fgets(buf,sizeof(buf),stdin);
- buf[strlen(buf)-1] = 0;
- write(fd,buf,strlen(buf));
- break;
- case 5:
- putchar('\n');
- printf("Read data from file.\n");
- memset(buf,0,sizeof(buf));
- n = read(fd,buf,sizeof(buf));
- buf[n-1] = 0;
- printf("Read %d byte : %s\n",n,buf);
- break;
-
- case 6:
- putchar('\n');
- help();
- break;
- }
- }
- return 0;
- }
- int main(int argc,char *argv[])
- {
- int fd;
- if(argc < 2)
- {
- fprintf(stderr,"usage : %s argv[1].\n",argv[0]);
- return -1;
- }
-
- if((fd = open(argv[1],O_RDWR | O_CREAT,0666)) < 0)
- {
- perror("Fail to open");
- return -1;
- }
-
- help();
- printf("\n");
- function(fd);
-
- return 0;
- }
这两个函数的功能都是复制已打开的文件描述符oldfd,返回一个新的文件描述符(newfd)。其本质,就是返回的新newfd在进程表项中的文件表项指针指向oldfd的文件表。例如,本来到达C点只有一条路A(oldfd),现在调用dup函数开辟了另一条路B(newfd),通过A或B都能达到C,而C还是原先的。
dup函数返回的新文件描述符一定是当前进程中,未用的最小文件描述符。
dup2函数的第二个参数可以指定返回的新文件描述符,如果第二个newfd已经被一个打开的文件使用,则会先关闭这个文件文件,是一个原子操作。if(oldfd == newfd),则dup2函数直接返回newfd。
案例一、dup函数
运行结果:
可以看出,dup返回的是当前进程未用的最小文件描述符。
案例二、使用dup2实现I/O重定向
运行结果:
- #include <stdio.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <string.h>
- #include <errno.h>
- int main(int argc,char *argv[])
- {
- int sfd,newfd,fd;
- if(argc < 2)
- {
- fprintf(stderr,"usage : %s argv[1]\n",argv[0]);
- return -1;
- }
-
- if((fd = open(argv[1],O_WRONLY | O_TRUNC | O_CREAT,0666)) < 0)
- {
- fprintf(stderr,"Fail to open %s:%s\n",argv[1],strerror(errno));
- return -1;
- }
-
- //复制文件描述符标准输出
- if((sfd = dup(STDOUT_FILENO)) < 0)
- {
- perror("Fail to dup");
- return -1;
- }
-
- //重定向操作
- if((newfd = dup2(fd,STDOUT_FILENO)) < 0)
- {
- perror("Fail to dup2");
- return -1;
- }
-
- close(fd);
- write(STDOUT_FILENO,"hello\n",6);
-
- if(dup2(sfd,newfd) < 0)
- {
- perror("Fail to dup2");
- return -1;
- }
- write(STDOUT_FILENO,"hello\n",6);
- return 0;
- }