一、文件描述符
    
    对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描叙符。当读、写一个文件时,用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可以打开或者创建一个文件

文件I/O相关函数-LMLPHP

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总是返回错误。

案例一、

  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <fcntl.h>
  4. #include <string.h>
  5. #include <errno.h>

  6. int main(int argc,char *argv[])
  7. {
  8.     FILE *fp;
  9.     int fd;
  10.     int ch;

  11.     if(argc < 2)
  12.     {
  13.         fprintf(stderr,"usage : %s argv[1]\n",argv[0]);
  14.         return -1;
  15.     }
  16.     
  17.     if((fd = open(argv[1],O_WRONLY | O_CREAT,0777)) < 0)
  18.     {
  19.         fprintf(stderr,"Fail to open %s : %s\n",argv[1],strerror(errno));
  20.         return -1;
  21.     }

  22.         return 0;
  23. }
运行结果:

文件I/O相关函数-LMLPHP

cyg文件原本不存在,运行程序后,创建了cyg文件,可以看到cyg文件的实际权限和指定的权限不一样。

注意:文件的实际权限 = mode & ~umask,umask系统默认值为0022,可以通过/etc/profile文件进行修改

rwxr-xr-x = 0777 & ~0022 =  755

案例 二

  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <fcntl.h>
  4. #include <string.h>
  5. #include <errno.h>

  6. int main(int argc,char *argv[])
  7. {
  8.     FILE *fp;
  9.     int fd;
  10.     int ch;

  11.     if(argc < 2)
  12.     {
  13.         fprintf(stderr,"usage : %s argv[1]\n",argv[0]);
  14.         return -1;
  15.     }
  16.     
  17.     if((fd = open(argv[1],O_WRONLY | O_CREAT,0777)) < 0)
  18.     {
  19.         fprintf(stderr,"Fail to open %s : %s\n",argv[1],strerror(errno));
  20.         return -1;
  21.     }
  22.         
  23.         if((fp = fdopen(fd,"r")) == NULL)
  24.     {
  25.         perror("Fail to fdopen");
  26.         return -1;
  27.     }
  28.         
  29.         return 0;
  30. }
运行结果:

文件I/O相关函数-LMLPHP

fdopen函数的功能是通过以打开的文件描述符,获取对应的流指针,这样就可以通过标准I/O的库函数来操作此文件。
以上我们在操作的过程中,open打开文件是以只写形式打开,而在fdopen的时候,指定的权限是只读,导致调用fdopen函数失败

注意:fdopen在指定权限时,应该和打开文件时的操作权限一样

案例三、

  1. #include <sys/types.h>
  2. #include <fcntl.h>
  3. #include <string.h>
  4. #include <errno.h>

  5. int main(int argc,char *argv[])
  6. {
  7.     FILE *fp;
  8.     int fd;
  9.     int ch;

  10.     if(argc < 2)
  11.     {
  12.         fprintf(stderr,"usage : %s argv[1]\n",argv[0]);
  13.         return -1;
  14.     }
  15.     
  16.     if((fd = open(argv[1],O_WRONLY | O_CREAT,0744)) < 0)
  17.     {
  18.         fprintf(stderr,"Fail to open %s : %s\n",argv[1],strerror(errno));
  19.         return -1;
  20.     }


  21.     if((fp = fdopen(fd,"w")) == NULL)
  22.     {
  23.         perror("Fail to fdopen");
  24.         return -1;
  25.     }

  26.     if((ch = fgetc(fp)) == EOF)
  27.     {
  28.         perror("Fail to fgetc");
  29.         return -1;
  30.     }


  31.     return 0;
  32. }
运行结果如下:

文件I/O相关函数-LMLPHP

我们打开的时候,指定的操作权限是只写,所以我们就行读操作就失败了。可以看出,打开文件时候指定的操作权限,限制了后期对文件的操作权限。


三、read函数

文件I/O相关函数-LMLPHP

函数功能:从打开的文件中读取数据
返回值  : 成功,读到的字节数; 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函数

文件I/O相关函数-LMLPHP

函数功能 :  向已经打开的文件写数据
返回值  :  成功,已写入的字节数;-1,出错
fd  :  文件描述符
buf  :  从内存的这个地址开始,向内存写count个字节到文件中。 
count : 指定写入的字节数

对于普通文件,写操作始于偏移量。如果打开文件时使用了O_APPEND,则每次写操作都将数据写入文件末尾。成功写入后,偏移量增加,增量为实际写入的字节数。

案例 -- 实现文件拷贝

  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <string.h>
  6. #include <errno.h>

  7. #define MAX 100

  8. int do_copy(int rfd,int wfd)
  9. {
  10.     char buf[MAX];
  11.     int n;

  12.     while(1)
  13.     {
  14.         if((n = read(rfd,buf,sizeof(buf))) < 0)
  15.         {
  16.             perror("Fail to read");
  17.             return -1;
  18.         }
  19.         
  20.         if(n == 0)
  21.         {
  22.             printf("Read end of file!\n");
  23.             return 0;
  24.         }

  25.         write(wfd,buf,n);
  26.     }

  27.     return 0;
  28. }

  29. int main(int argc,char *argv[])
  30. {
  31.     int rfd,wfd;

  32.     if(argc < 3)
  33.     {
  34.         fprintf(stderr,"usage : %s argv[1] argv[2].\n",argv[0]);
  35.         return -1;
  36.     }

  37.     if((rfd = open(argv[1],O_RDONLY)) < 0)
  38.     {
  39.         fprintf(stderr,"Fail to open %s : %s\n",argv[1],strerror(errno));
  40.         return -1;
  41.     }

  42.     if((wfd = open(argv[2],O_WRONLY | O_CREAT,0666)) < 0)
  43.     {
  44.         fprintf(stderr,"Fail to open %s : %s\n",argv[2],strerror(errno));
  45.         return -1;
  46.     }

  47.     if(do_copy(rfd,wfd) < 0)
  48.     {
  49.         return -1;
  50.     }

  51.     return 0;
  52. }
运行结果:

文件I/O相关函数-LMLPHP

五.lseek函数

所有打开的文件都有一个当前文件偏移量(current file offset),以下简称cfo,cfo通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数。读写操作通常开始于cfo,并且使cfo增加所读写的字节数。文件被打开时,cfo会被初始化为0,除非使用了O_APPEND。

文件I/O相关函数-LMLPHP


返回值:成功,文件的当前位移;-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 

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <errno.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6. #include <sys/stat.h>
  7. #include <fcntl.h>

  8. #define MAX 100

  9. int half_store(int fd,int fd1,int fd2)
  10. {
  11.     char ch;
  12.     int len = 0,count = 0,n = 0;

  13.     len = lseek(fd,0,SEEK_END);
  14.     lseek(fd,0,SEEK_SET);
  15.     
  16.     while(1)
  17.     {
  18.         if((n = read(fd,&ch,1)) < 0)
  19.         {
  20.             perror("Fail to read");
  21.             return -1;
  22.         }
  23.         
  24.         if(n == 0)
  25.         {
  26.             printf("Read end of file.\n");
  27.             return 0;
  28.         }
  29.         
  30.         count ++;

  31.         if(count <= len / 2)
  32.         {
  33.             write(fd1,&ch,1);
  34.         
  35.         }else{
  36.             
  37.             write(fd2,&ch,1);
  38.         }
  39.     }
  40.     
  41.     return 0;
  42. }

  43. int main(int argc,char *argv[])
  44. {
  45.     int fd,fd1,fd2;
  46.     
  47.     if(argc < 3)
  48.     {
  49.         fprintf(stderr,"usage : %s argv[1] argv[2].\n",argv[0]);
  50.         return -1;
  51.     }

  52.     if((fd = open(argv[1],O_RDONLY)) < 0)
  53.     {
  54.         fprintf(stderr,"Fail to open %s : %s.\n",argv[1],strerror(errno));
  55.         return -1;
  56.     }

  57.     if((fd1 = open(argv[2],O_WRONLY | O_CREAT,0666)) < 0)
  58.     {
  59.         fprintf(stderr,"Fail to open %s : %s.\n",argv[2],strerror(errno));
  60.         return -1;
  61.     }

  62.     if((fd2 = open(argv[3],O_WRONLY | O_CREAT,0666)) < 0)
  63.     {
  64.         fprintf(stderr,"Fail to open %s : %s.\n",argv[3],strerror(errno));
  65.         return -1;
  66.     }

  67.     if(half_store(fd,fd1,fd2) < 0)
  68.     {
  69.         printf("Fail to half_store.\n");
  70.         return -1;
  71.     }

  72.     return 0;
  73. }
运行结果如下:

文件I/O相关函数-LMLPHP

六、文件共享 

UNIX支持在不同进程间共享打开文件。

内核使用三种数据结构表示打开的文件,他们之间的关系决定了多进程的文件共享操作。

A.进程表 : 每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项,与之关联的是文件描述符标志,和一个指向文件表项的指针。

B.文件描述符表:内核为所有打开文件维持一张文件表,每个表项包含文件标志,当前偏移量,指向该文件v结点表项的指针。打开某文件的每个进程得到一个该文件表项。

C.v结点:每个打开的文件都有一个v结点结构。(保存在磁盘里,读入内存的)。Linux中没有v结点的概念,i结点就是v结点,对于一个给定文件,i结点只有一个。

三者关系如下图,当一个进程打开两个文件时

文件I/O相关函数-LMLPHP

如果两个独立进程各自打开了同一个文件。我们假定第一个进程使该文件在文件描述符3上打开,而另一个进程则使此文件在文件描述符4上打开。打开此文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v结点表项。每个进程都有自己的文件表项的一个理由是:这种安排使每个进程都有它自己的对该文件的当前位偏移量。

文件I/O相关函数-LMLPHP

在完成每个write后,在文件表项中的当前文件位偏移量即增加所写的字节数。如果当前文件位移量超过了当前文件长度,则在i结点表项中的当前文件长度被设置为当前文件位移量(也就是该文件加长了)。

如果用O_APPEND标志打开了一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有添写标志标志的文件执行写操作时,在文件表项中的当前文件位移量首先被设置为i 节点表项中的文件长度。这就使得每次写的数据都添加到文件的当前尾端处。

lseek函数只修改文件表项中的当前文件位移量,没有进行任何I/O操作。

若一个文件使用lseek被定位到文件当前的尾端,则文件表项中的当前文件位移量被设置为i节点表项中的当前文件长度。

案例:

  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <string.h>

  6. #define MAX 50

  7. void help()
  8. {
  9.     printf("CMD 1 : print file pointer position.\n");
  10.     printf("CMD 2 : make file pointer to head of file.\n");
  11.     printf("CMD 3 : make file pointer to end of file.\n");
  12.     printf("CMD 4 : write data to file.\n");
  13.     printf("CMD 5 : read data from file.\n");
  14.     printf("CMD 6 : help info.\n");

  15.     return;
  16. }

  17. int function(int fd)
  18. {
  19.     int cmd;
  20.     char buf[MAX];
  21.     int n;
  22.     
  23.     while(1)
  24.     {
  25.         putchar('\n');
  26.         printf("Input cmd : ");
  27.         if(scanf("%d",&cmd) == 0)
  28.         {
  29.             printf("Quit\n");
  30.             break;
  31.         }

  32.         while(getchar() != '\n');

  33.         switch(cmd)
  34.         {
  35.         case 1:
  36.             putchar('\n');
  37.             printf("CURRENT : %d\n",lseek(fd,0,SEEK_CUR));
  38.             break;

  39.         case 2:
  40.             putchar('\n');
  41.             printf("Start head of file.\n");
  42.             lseek(fd,0,SEEK_SET);
  43.             break;

  44.         case 3:
  45.             putchar('\n');
  46.             printf("Start end of file.\n");
  47.             lseek(fd,0,SEEK_END);
  48.             break;

  49.         case 4:
  50.             putchar('\n');
  51.             printf("Write data to file.\n");
  52.             memset(buf,0,sizeof(buf));
  53.             printf(">");
  54.             fgets(buf,sizeof(buf),stdin);
  55.             buf[strlen(buf)-1] = 0;
  56.             write(fd,buf,strlen(buf));
  57.             break;

  58.         case 5:
  59.             putchar('\n');
  60.             printf("Read data from file.\n");
  61.             memset(buf,0,sizeof(buf));
  62.             n = read(fd,buf,sizeof(buf));
  63.             buf[n-1] = 0;
  64.             printf("Read %d byte : %s\n",n,buf);
  65.             break;
  66.             
  67.         case 6:
  68.             putchar('\n');
  69.             help();
  70.             break;
  71.         }
  72.     }

  73.     return 0;
  74. }

  75. int main(int argc,char *argv[])
  76. {
  77.     int fd;

  78.     if(argc < 2)
  79.     {
  80.         fprintf(stderr,"usage : %s argv[1].\n",argv[0]);
  81.         return -1;
  82.     }
  83.     
  84.     if((fd = open(argv[1],O_RDWR | O_CREAT,0666)) < 0)
  85.     {
  86.         perror("Fail to open");
  87.         return -1;
  88.     }
  89.     
  90.     help();
  91.     printf("\n");

  92.     function(fd);
  93.     
  94.     return 0;
  95. }
七、dup和dup2函数

文件I/O相关函数-LMLPHP

这两个函数的功能都是复制已打开的文件描述符oldfd,返回一个新的文件描述符(newfd)。其本质,就是返回的新newfd在进程表项中的文件表项指针指向oldfd的文件表。例如,本来到达C点只有一条路A(oldfd),现在调用dup函数开辟了另一条路B(newfd),通过A或B都能达到C,而C还是原先的。

dup函数返回的新文件描述符一定是当前进程中,未用的最小文件描述符。
dup2函数的第二个参数可以指定返回的新文件描述符,如果第二个newfd已经被一个打开的文件使用,则会先关闭这个文件文件,是一个原子操作。if(oldfd == newfd),则dup2函数直接返回newfd。

案例一、dup函数

文件I/O相关函数-LMLPHP

运行结果:

文件I/O相关函数-LMLPHP

可以看出,dup返回的是当前进程未用的最小文件描述符。

案例二、使用dup2实现I/O重定向

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <string.h>
  7. #include <errno.h>

  8. int main(int argc,char *argv[])
  9. {
  10.     int sfd,newfd,fd;

  11.     if(argc < 2)
  12.     {
  13.         fprintf(stderr,"usage : %s argv[1]\n",argv[0]);
  14.         return -1;
  15.     }
  16.     
  17.     if((fd = open(argv[1],O_WRONLY | O_TRUNC | O_CREAT,0666)) < 0)
  18.     {
  19.         fprintf(stderr,"Fail to open %s:%s\n",argv[1],strerror(errno));
  20.         return -1;
  21.     }
  22.     
  23.         //复制文件描述符标准输出
  24.     if((sfd = dup(STDOUT_FILENO)) < 0)
  25.     {
  26.         perror("Fail to dup");
  27.         return -1;
  28.     }
  29.         
  30.         //重定向操作    
  31.     if((newfd = dup2(fd,STDOUT_FILENO)) < 0)
  32.     {
  33.         perror("Fail to dup2");
  34.         return -1;
  35.     }
  36.     
  37.     close(fd);

  38.     write(STDOUT_FILENO,"hello\n",6);
  39.     
  40.     if(dup2(sfd,newfd) < 0)
  41.     {
  42.         perror("Fail to dup2");
  43.         return -1;
  44.     }

  45.     write(STDOUT_FILENO,"hello\n",6);

  46.     return 0;
  47. }
运行结果:

文件I/O相关函数-LMLPHP




12-28 07:44