Chinaunix首页 | 论坛 | 博客
  • 博客访问: 334672
  • 博文数量: 106
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 861
  • 用 户 组: 普通用户
  • 注册时间: 2013-09-10 08:32
文章分类

全部博文(106)

文章存档

2016年(11)

2015年(93)

2013年(2)

分类: LINUX

2015-08-12 14:57:52

    当用户访问有关普通文件或者目录文件的内容的时候,他实际上访问存储在硬件块设备上的一些数据。从这个意义上说,文件系统是硬盘分区物理组织的用户级视图。
    在linux中,一切都是文件,文件为操作系统服务和设备提供了一个简单而统一的接口,这就意味者程序可以像使用文件那样使用各种设备。Linux内核本身并不是一个进程,而是进程的管理者。每个系统调用都设置了一组识别进程请求的参数,然后执行与硬件相关的CPU指令来完成从用户态到内核态的转换。大多数情况下对于文件的操作只用到open,write,lseek,read,close五个系统调用。本文通过一个简单的例子来介绍这五个调用及关联内容。

    先看例子:
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>

int
main( void )
{
  int file_des = open( "my_file.txt", O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IXUSR | S_IXOTH );
 
  char *write_buf = "zhujiangfeng\n";
  if ( write( file_des,write_buf, strlen( write_buf )) != strlen( write_buf ) )
  {
    write( STDERR_FILENO, "WRITE ERROR!\n", 13 );
    exit( 0 );
  }

  if ( lseek( file_des, 4, SEEK_END ) == -1 )
  {
    write( STDERR_FILENO, "SEEK ERROR!\n", 11 );
    exit( 0 );
  }
  write( file_des, "AAAAAA", 6 );

  lseek( file_des, 0, SEEK_SET );
  char read_buf[50];

  if ( read( file_des, read_buf, 50 ) == -1 )
  {
    write( STDERR_FILENO, "READ ERROR!\n", 12 );
    exit( 0 );
  }
  write( STDOUT_FILENO, read_buf, 50 );

  close( file_des );
  exit( 1 );
}
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
  这是一个简单的读写文件的例子,先创建一个文本文件,写入一些内容,再把文本内容输出到标准输出,下面开始分析这个例子:
  1 int file_des = open( "my_file.txt", O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IXUSR | S_IXOTH );
    以读写方式打开一个新建文件 my_file.txt,指定其访问权限为 文件属主具有读,写,执行权限,组用户没有任何权限,其他用户只有执行权限。

    open调用用于创建或打开文件,返回一个文件描述符。
    #include<fcntl.h>
    #include<sys/stat.h>
    #include<sys/types.h>
    int open( const char *path, int oflags );
    int open( const char *path, int oflags, mode_t mode );

    第一个oen用于打开已有的文件,第二个open用于创建新文件。
    (1)oflags参数说明了文件的打开方式,值为以下一个或多个常量的“或”运算(这些常量定义在<fcntl.h>):
    O_RDONLY 只读打开
    O_WRONLY 只写打开
    O_RDWR   读,写打开
    以上常量为必选,且只能选一个,下列常量为可选:
    O_APPEND 在文件尾端追加
    O_TRUNC  将文件长度截短为0
    O_CREATE 若文件不存在,按照参数mode指定的访问权限创建
    O_EXCL   测试要创建的文件是否存在,和O_CREATE一起使用,使得文件的测试和创建是一个院子操作
    (2)mode参数指定了新建文件的访问权限,值为以下一个或多个标识的“或”运算(这些标识定义在<sys/stat.h>):
    S_IRUSR  文件属主具有读权限
    S_IWUSR  文件属主具有写权限
    S_IXUSR  文件属主具有执行权限
 
    S_IRGRP  文件所属组具有读权限
    S_IWGRP  文件所属组具有写权限
    S_IXGRP  文件所属组具有执行权限

    S_IROTH  其他用户具有读权限
    S_IWOTH  其他用户具有写权限
    S_IXOTH  其他用户具有执行权限
    注:mode参数实际上是设置文件访问权限的请求,该请求是否被允许取决于此时umask的设置。
    (3)如果两个程序同时打开同一个文件,会得到两个不同的文件描述符。如果都进行写操作,他们的数据将会相互覆盖,而不是交织在一起。两个文件对读写的起始位置(偏移值)也有各自的理解。文件锁可以防止此情况的发生,以后将会提到这个概念。

    2 write( file_des,write_buf, strlen( write_buf )) != strlen( write_buf )
     将缓冲区write_buf中的所有字节写入与文件描述符file_des关联的文件中,并且判断是否成功写入。

    #include<unistd.h>
    size_t write( int file_des, const void *buf, size_t bytes );

    (1)write的返回值可能会小于bytes,但这并不一定是个错误,需要检查全局变量errno来确定。

    3  if ( lseek( file_des, 4, SEEK_END ) == -1 )
       {
         write( STDERR_FILENO, "SEEK ERROR!\n", 11 );
         exit( 0 );
       }
     将文件的读写偏移量推进到超过文件结尾4个字节处,如果失败,像标准输出输出错误信息。
        注:对于普通文件,可以顺序和随机访问;而对设备文件命名管道文件,通常只能顺序访问。在这两种访问方式中,内核把文件指针存放在打开文件对象中,也就是说,当前位置就是下一次进行读或者写操作的位置。顺序访问是文件的默认访问方式,即:read()和write()系统调用总是从文件指针的当前位置开始读或者写。为了修改文件指针的值,必须在程序中显式地调用lseek()系统调用。当打开文件的时候,内核让文件指针指向文件的第一个字节(偏移量为0)。
     #include<unistd.h>
     #include<sys/types.h>
     off_t lseek( int file_des, off_t off_set, int whence );
     lseek用于设置文件的读写偏移量,返回新的读写偏移量。
        file_des:打开文件的文件描述符
        off_set:指定一个有符号整数值,用来计算文件指针的新位置。

        whence :指定文件指针新位置的计算方式:可以是offset加0,表示文件指针从文件头移动;也可以是offset加文件指针的当前位置,表示文件从当前位置移动;还可以是offset加文件最后一个字节的位置,表示文件指针从文件末尾开始移动。

     (1)off_t是一个与具体实现有关的类型,定义在<sys/types.h>中;
     (2)whence的取值如下:
        SEEK_SET  将文件的读写偏移量设置为距离文件开始处off_set个字节
        SEEK_CUR  将文件的读写偏移量设置为当前值加上off_set,off_set可正可负
        SEEK_END  将文件的读写偏移量设置为文件长度加上off_set,off_set可正可负
     (3)当在超过文件尾端之后写入时,就会在文件中形成一个空洞。文件空洞并不占用磁盘空间,处理方式与文件系统的实现有关。可以用$od -c file 查看空洞文件的内容。
     (4)STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO分别对应程序的标准输入,标注输出,标准出错。这些常量定义在<unistd.h>中。

    4  #include<unistd.h>
       int close( int file_des );
       终止文件描述符file_des与对应文件的关联。

       (1)文件描述符被释放并重新利用;
      (2)文件关闭后,进程还会释放加在文件上的所有记录锁;
      (3)进程终止后,内核会自动关闭所有打开的文件。

    5 其他与文件有关的系统调用
      (1)#include<unistd.h>
          #include<sys/stat.h>
          #include<sys/types.h>
          int fstat( int file_des, struct stat *buf );
          int stat( const char *path, struct stat *buf );
          int lstat( const char *path, struct stat *buf );
       这三个系统调用用于获取文件的信息并填充在struct stat中,成功返回0,出错返回-1。当path指向的对象是符号链接时,stat返回的是该链接指向的文件的信息,而lstat返回的是该链接的信息。这三个系统调用将在以后作为一个专题来讨论。
      (2)#include<unistd.h>
          int dup( int file_des );
          int dup2( int file_des, int file_des2 );
        dup系统调用复制文件描述符file_des,返回一个新的最小值的可用文件描述符。通过两个或多个文件描述符可以实现在文件的不同位置读写数据。dup2明确指定将file_des复制为file_des2。这在通过管道进行进程间通信时很有用。

一、什么是文件

在讲述文件操作之前,我们首先要知道什么是文件。看到这个问题你可能会感觉到可笑,因为对于用过计算机的人来说,文件是最简单不过的概念了,例如一个文本是一个文件,一个work文档是一个文件等。但是在Linux中,文件的概念还远不止于这些,在Linux中,一切(或几乎一切)都是文件。文件包括很多的内容,例如:大家知道的普通文件是文件,目录也是一个文件,设备也是一个文件,管道也是一个文件等等。对于目录、设备这些的操作也可以完全等同于对纯文本文件的操作,这也是Linux非常成功的特性之一吧。

二、系统调用

1、文件描述符
文件描述符是一些小数值,你可以通过它们访问的打开的文件设备,而有多少文件描述符可用取决于系统的配置情况。但是当一个程序开始运行时,它一般会有3个已经打开的文件描述符,就是
0:标准输入
1:标准输出
2:标准错误
那些数学(即0、1、2)就是文件描述符,因为在Linux上一切都是文件,所以标准输入(stdin),标准输出(stdout)和标准错误(stderr)也可看作文件来对待。

2、系统调用常用函数

A、open系统调用

open函数的原型为:
int open(const char *path, int oflags);
int open(const char *path, int oflags, mode_t mode);

path,是包括路径的完整文件名,oflags是文件打开方式(只读、只写还是可读并可写等),他也指定是否应当创建一个不存在的文件,mode用于设定文件的访问权限。具体的可选参数,可以自己查看手册页,这里不一一详述。

open建立了一条到文件或设备的访问路径,如果调用成功,返回一个可以被read、write等其他系统调用的函数使用的文件描述符,而且这个文件描述是唯一的,不与任何其他运行中的进程共享,在失败时返回-1,并设置全局变量errno来指明失明的原因。

B、write系统调用

write函数的原型为:
size_t write(int fildes, const void *buf, size_t nbytes);

write的作用是把缓冲区buf的前nbytes个字节写入到文件描述符fildes关联的文件中,返回实际写入的字节数。返回0表示没有写入任何数据,返回-1表示调用中出现了错误,错误代码保存在errno中。

注:fildes一定要是在open调用中返回的创建的文件描述符,或者是0、1、2等标准输入、输出或标准错误。

C、read系统调用

read函数的原型为:
size_t read(int fildes, void *buf, size_t nbytes);

read系统调用的作用是从与文件描述符相关的文件里读入nbytes个字节的数据,并把它们放到数据区buf中,其起始位置为打开文件的offset字段的的当前值。返回实际所读的字节数,失败时返回-1。

D、close系统调用

close调用的函数原型为:
int close(int fildes);
close函数的作用是释放和文件描述符fildes相对应的打开文件对象及其关联。当一个进程终止的时候,内核回关闭其所有仍然打开着的文件。

要重新命名或者删除一个文件的时候,进程不需要打开它。实际上,这样的操作并没有对这个文件的内容起作用,而是对一个或多个目录的内容起作用。例如,系统调用:
res = rename(oldpath,newpath)改变了文件链接的名字
res = unlink(pathname)减少了文件链接数,删除了相应的目录项。只有当链接数为0的时候,文件才被真正删除。

E、例子

说了这么多,我就给出一个完整的例子吧,就是从一个数据文件(里面有1M个‘0’字符)逐个复制到别一个文件。文件名为copy_system.c,代码如下:
  1. #include <unistd.h>   
  2. #include <sys/stat.h>   
  3. #include <fcntl.h>   
  4. #include <stdlib.h>   
  5.   
  6. int main()  
  7. {  
  8.     char c = '\0';  
  9.     int in = -1, out = -1;  
  10.       
  11.     //以只读方式打开数据文件   
  12.     in = open("Data.txt", O_RDONLY);  
  13.     //以只写方式创建文件,如果文件不存在就创建一个新的文件   
  14.     //文件属主具有读和写的权限   
  15.     out = open("copy_system.out.txt", O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);  
  16.     while(read(in, &c, 1) == 1)//读一个字节的数据   
  17.         write(out, &c, 1);//写一个字节的数据   
  18.   
  19.     //关闭文件描述符   
  20.     close(in);  
  21.     close(out);  
  22.     return 0;  
  23. }  


三、标准I/O库
有过C编程经历的人都会知道stdio头文件,它就是C语言的标准IO库,在标准IO库中,与底层文件描述符相对应的是流,它被实现为指向结构FILE的指针。IO库的函数有很多,为了与前面的内容对应,这里还是只讲与前面四个函数相对应的函数,其他的函数,你可以查一查手册页。

A、fopen库函数

fopen库函数的原型为:
FILE* fopen(const char *filename, const char *mode);

它与底层系统调用open类似,成功时返回一个非空指针。失败时返回NULL。

B、fread库函数

fread库函数的原型为:
size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);

它与底层调用read相似,其作用是从stream读取nitems个长度为size的数据到ptr所指向的缓冲区中。返回值是成功读到缓冲区中的记录个数。

注:stream为用fopen函数返回的文件结构指针。

C、fwrite库函数

fwrite库函数的原型:
size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream);

它与底层调用write相似,其作用是从ptr指向的缓冲区中读取nitems个长度为size到数据,并把它们写到stream所对应的文件中。

D、fclose库函数

fclose库函数的原型为:
int fclose(FILE *stream);

它与系统调用close相似,其作用是关闭指定的文件流stream。

例子
同样地,下面是前一个例子的另一个实现版本,它实现的功能与先前的例子一样,不过使用的是标准I/O库,而不是系统调用,文件名为copy_stdio.c代码如下:
  1. #include <stdio.h>   
  2. #include <stdlib.h>   
  3.   
  4. int main()  
  5. {  
  6.     int c = 0;  
  7.     FILE *pfin = NULL;  
  8.     FILE *pfout = NULL;  
  9.   
  10.     //以只读方式打开数据文件   
  11.     pfin = fopen("Data.txt""r");  
  12.     //以只写方式打开复制的新文件   
  13.     pfout = fopen("copy_stdio.out.txt""w");  
  14.       
  15.     while(fread(&c, sizeof(char), 1, pfin))//读数据   
  16.         fwrite(&c, sizeof(char), 1, pfout);//写数据   
  17.     //关闭文件流   
  18.     fclose(pfin);  
  19.     fclose(pfout);  
  20.     return 0;  
  21. }  


当然这里你也可以用其他的库函数来完成工作,如:用fgetc代替fread,用fputc代替fwrite等。

四、文件描述符和文件流的关系
每个文件流都对应一个底层文件描述符,你可以把底层输入输出操作与高层文件流操作混合使用,但是一般不要这样做,因为数据缓冲的后果难以预料。我们可以通过调用fileno函数(原型为:int fileno(FILE *stream))来确定文件流使用的底层文件描述符,它返回指向文件流的文件描述符。相反地,你可以通过调用函数fdopen(原型为FILE* fdopen(int fildes, const char* mode))来在一个已经打开的文件描述符上创建一个新的文件流,mode参数与fopen函数的完全一样,同时它必须符合该文件在最初打开时所设定的访问模式。

但是在Linux下的编程,系统调用用得比较多一些,因为很多时候系统调用能提供更多的灵活性和更加强大的功能,有些操作是一定要使用系统调用,例如,创建文件读写锁时就一定要使用系统调用。

五、系统调用与标准I/O的性能比较
就拿本例子中的代码来比较,两个例子编译后生成的可执行文件的文件名分别为:copy_system.exe和copy_stdio.exe,在Linux下用time命令来测试其运行时间如下:


从测试结果可以看出,系统调用的效率比库函数要低很多。为什么呢?

因为使用系统调用会影响系统的性能。与函数调用相比,系统调用时,Linux必须从运行用户代码切换到执行内核代码,然后再返回用户代码,所以系统调用的开销要比普通函数调用大一些。然而也是有办法减少这种开销的,就是在程序中尽量减少系统调用的的次数,并且让每次系统调用完成尽量多的工作。

而库函数为什么做同样的事情效率却会高这么多呢?这是因为库函数在数据满足数据块长度(或buffer长度)要求时才安排执行底层系统调用,从而减少了系统调用的次数,也让每次的系统调用做了尽量多的事情,所以效率就比较高。

六、提高系统调用的简单方法举例
用回每一个例子(coy_system.c)的代码,略加修改就能提高我们的效率,例如一次读1024个字节,修改后保存文件名为copy_system2.c,代码如下:

  1. #include <unistd.h>   
  2. #include <sys/stat.h>   
  3. #include <fcntl.h>   
  4. #include <stdlib.h>   
  5.   
  6. int main()  
  7. {  
  8.     char buff[1024];  
  9.     int in = -1, out = -1;  
  10.     int nread = 0;  
  11.   
  12.     in = open("Data.txt", O_RDONLY);  
  13.     out = open("copy_system2.out.txt", O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR);  
  14.     //一次读写1024个字节   
  15.     while((nread = read(in, buff, sizeof(buff))) > 0)  
  16.         write(out, buff, nread);  
  17.   
  18.     close(in);  
  19.     close(out);  
  20.     return 0;  
  21. }  


生成的可执行文件为copy_system2.exe,使用time命令查看其执行时间,如下:


比较下可以看出,其性能改善了一个数量级,其效率甚至比用库函数一个一个字符复制来来得高效,至少在我的机子上是这样。


阅读(740) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~