琢思磋文轩

学问之道贵能下人求告为善,赡才之径假人所长补已之短

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

                                              Unix环境高级程序设计入门
                                                              ----文件系统的相关编程(上)

                                                          一、关于目录

  不管是何种操作系统,一提到其中的文件系统首先想到的自然是目录和文件。在Unix系统中一切皆可视为文件,目录是一种特殊的文件。在笔者前已发表的《Unix操作系统的入门与基础》一文中,曾介绍过用户主目录、工作目录以及绝对路径、相对路径的概念,我们也已经知道了使用pwd命令可以获得当前工作目录的绝对路径,那么如何在程序中实现类似于pwd命令的功能呢?这里需要用到getcwd()函数,它的定义是:
   
  #include <unistd.h>
  char* getcwd(char* buf, size_t size);  //成功返回buf,失败返回NULL
   
此函数中的第一个参数buf数组用于存放当前工作目录路径名的字符串,size是指这个buf数组最多能装多少数据,而函数返回值与放入buf中的内容是相同的。要注意的是,该缓存必须有足够的长度以容纳绝对路径名再加上一个“/0”终止字符,否则将会返回出错。
  对于目录的操作,最常见的就是打开目录、读取目录信息、关闭目录,与之对应的函数分别是:

  #include <sys/types.h>
  #include <dirent.h>
  DIR* opendir(const char* dirname);    //成功时返回指针,失败返回NULL
  struct dirent* readdir(DIR* dirp);         //成功时返回指针,失败返回NULL
  int closedir(DIR* dirp);                      //成功时返回0,失败返回-1
  
使用opendir()函数打开不存在的目录或者对目录没有访问权限,以及对普通文件使用此函数都将返回NULL。opendir()函数成功操作后将返回指向DIR结构的指针,而DIR结构用来保存被读取目录的相关信息。定义在头文件<dirent.h>中的dirent结构最常用到的成员是d_name,它可以保存文件名。
  现来看下面的一个例程序:
  [程序1]

  #include <iostream>
  #include <unistd.h>
  #include <sys/types.h>
  #include <dirent.h>
  #include <errno.h>
  using namespace std;
  int main()
  {
    DIR* dp;
    cout << " Please enter a dir name: ";
    char name[255];
    memset(name,0x00,255);
    cin >> name;
    cout << " ----------------- " << endl;
    dp = opendir(name);
    if(dp == NULL)
    {
      cout << errno << " [" <<strerror(errno) << "]" << endl;
      return -1;
    }
    dirent* dirp;
    while((dirp = readdir(dp))!= NULL)
    {
      cout << dirp->d_name << endl;
    }
    closedir(dp);
    return 0;
  }
 
  在Unix系统中,一旦出现程序执行失败的情况,系统会自动设置一个名为errno的全局变量,用于记录错误的出错ID号。使用strerror(errno)可以获得指定错误的描述信息,不过在此之前一定要包含errno.h的头文件。编译执行程序1时,如果输入一个已存在且有访问权限的目录名,程序会列出此目录下的所有子目录名以及文件名;如果输入的是一个文件名,则会输出20 [Not a directory];如果输入的是一个不存在的目录名,则会输出2 [No such file or directory]。
 
                                           
二、关于文件

  在介绍文件之前,先来引入文件描述符这一概念。文件描述符是一非负整数,内核以此来标识一个特定进程正在操作的文件。每当打开一个现存文件或创建一个新文件时,内核将向进程返回一个文件描述符,以供读、写文件时使用。对于进程而言,内核会在每个进程空间中维护一文件描述符表,所有打开的文件都将通过此表中的文件描述符来引用。
  按照规定,文件描述符0与标准输入cin相对应,1与标准输出cout相对应,2与标准出错输出cerr相对应。除此之外,文件描述符表中的每一文件描述符都对应着文件表中的一个文件表项。内核为所有已打开的文件维护一张文件表,每个文件表项包含文件状态标志(读、写、增写、同步、非阻塞等)、当前文件位移量、V节点指针,其中V节点指针指向V节点表。V节点表由V节点项组成,每一V节点项中主要包含V节点信息、I节点信息(包括文件所在的设备、文件存储在硬盘的某一扇区某一磁道等信息)、当前文件长度等。在Unix系统中,一个文件对应着V节点表中惟一的一个V节点项。也就是说,如果两个独立的进程打开同一个文件,在文件表中会有两个文件表项,但这两个文件表项都指向同一个V节点表项。图1显示了文件描述符表、文件表、V节点表三者之间的关系。
   
        
  如果两个独立的进程各自打开了同一文件,则文件描述符表、文件表、V节点表三者之间的关系如图2中所示。

    
    
  Unix系统中文件I/O只需用到5个函数:open、read、write、lseek和close。下面将一一进行介绍。
   
  1、open函数可以打开或创建一个文件,它的定义是:
   
  #include <sys/types.h>
  #include <sys/stat.h>
  #include <fcntl.h>
  int open(const char* pathname, int oflag, ...); //成功时返回文件描述符,失败返回-1
 
  使用open函数成功时返回的文件描述符一定是最小的未用描述符数字。open函数的参数pathname是所要打开或创建的文件的名称,可以是相对路径,也可以是绝对路径,不给路径时默认为当前目录。参数oflag由一个或多个选择项组成,用以指明打开的模式,常用的有O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(可读可写),此三项互相排斥,我们使用时应该指定为其中的一项。此外,还有以下一些选择项可以选择:O_APPEND(每次写入的数据都加到文件的尾端)、O_CREAT(若文件不存在则创建它;使用此选择项时,需同时提供第三个参数来说明该新文件的存取权限)、O_EXCL(与O_CREAT一起使用时,文件不存在则创建文件,若文件已存在则报错)。open函数的第三个参数写为...,是因为仅当创建新文件时才使用此参数,用以表示所创建文件的访问权限,如0700代表只有用户自身可读可写可执行,其他人无权访问。
  [程序2]
 
  #include <iostream>
  #include <unistd.h>
  #include <sys/types.h>
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <errno.h>

  
using namespace std;

  int main()
  {
    int i = open("info.txt",O_RDWR);
    cout << "fd = " << i << endl;

    int ii = open("new.txt", O_CREAT|O_EXCL,0700);
    if(ii<0) cout << strerror(errno) << endl;
    else     cout << "Create file successful" << endl;

    return 0;
  }
 
  假设当前目录中已存在文件info.txt,编译执行程序2,大家会看到如下的运行结果:
 
  fd = 3
  Create file successful
 
  再次执行程序2,运行结果则是:
 
  fd = 3
  File exists
  
  2、close函数可以关闭一个打开的文件,它的定义是:
 
  #include <unistd.h>
  int close (int fd); //成功返回0,失败返回-1
 
  使用close函数关闭一个文件的同时也会释放该文件上的所有记录锁。不过,当一个进程终止时,它所有的打开文件都由内核自动关闭,因此程序通常利用这一特性而不用显式调用close来关闭文件了。
 
  3、read函数可以从打开的文件中读取数据,它的定义是:
 
  #include <unistd.h>
  ssize_t read(int fd, void* buf, size_t nbytes); //成功时返回真正读取的字节数,失败则返回-1
 
  read函数是通过给定的文件描述符,在文件描述符所对应的文件表项中获得当前文件位移量,然后从文件的当前位移量处开始,读取给定长度的数据放入buf中。
 
  4、write函数可以向打开的文件中写入数据,它的定义是:
 
  #include <unistd.h>
  ssize_t write(int fd, void* buf, size_t nbytes); //成功时返回真正写入的字节数,失败则返回-1
 
  write函数是从文件的当前位移量处开始,将buf中的数据写入文件中。下面将程序2中的代码做一修改,并假设info.txt文件中已有数据ABCDE。
  [程序3]
 
  #include <iostream>
  #include <unistd.h>
  #include <sys/types.h>
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <errno.h>

  using namespace std;

  int main()
  {
    int i = open("info.txt",O_RDWR);
    cout << "fd = " << i << endl;

    int ii = open("New.txt", O_CREAT|O_EXCL,0700);
    if(ii<0) cout << strerror(errno) << endl;
    else     cout << "Create file successful" << endl;

    char buf[500];
    memset(buf,0x00,500);
    cout << read(i,buf,500) << "\t";
    cout << buf << endl;
    memset(buf,0x00,500);
    read(0,buf,500);                           //从键盘读取数据
    cout << write(1,buf,strlen(buf)) << endl;  //向屏幕写入数据
    return 0;
  }
 
  编译执行程序3,大家会看到如下的运行结果:
 
  fd = 3
  File exists
  6    ABCDE
  12345   (键盘输入)
  12345
  6
 
  5、lseek函数可以对一个打开文件的当前位移量进行显式地调整,它的定义是:
 
  #include <sys/types.h>
  #include <unistd.h>
  off_t lseek(int fd, off_t offset, int whence);  //成功时返回新的文件位移量,失败则返回-1
 
  在上文中我们已多次提到“当前文件位移量”这一名词,这里再作一简要解释。当前文件位移量是一非负整数,用以度量从文件开始处到某一位置的字节数。每个打开文件,都有一个与其相关联的“当前文件位移量”。打开一个文件时,除非指定O_APPEND选择项,否则系统默认该位移量被设置为0。通常,读、写操作都从当前文件位移量处开始,并使位移量增加所读或写的字节数。lseek函数中的第二参数是表示将要移动的字节数,第三个参数是移动的策略。若whence是SEEK_SET,则将该文件的位移量设置为距文件开始处offset个字节;若whence是SEEK_CUR,则将该文件的位移量设置为其当前值加offset, offset,可为正或负;若whence是SEEK_END,则将该文件的位移量设置为文件长度加offset, offset,可为正或负。
  大家可能会对上述中的一种情况感兴趣,那就是采用SEEK_END再往后移动会出现怎么样的情况呢?带着这个问题,我们对程序3做如下修改。
  [程序4]
 
  #include <iostream>
  #include <unistd.h>
  #include <sys/types.h>
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <errno.h>

  using namespace std;

  int main()
  {
    int i = open("info.txt",O_RDWR);
    cout << "fd = " << i << endl;

    int ii = open("New.txt", O_CREAT|O_EXCL,0700);
    if(ii<0) cout << strerror(errno) << endl;
    else cout << "Create file successful" << endl;

    char buf[500];
    memset(buf,0x00,500);
    cout << read(i,buf,500) << "\t";
    cout << buf << endl;
    memset(buf,0x00,500);
    read(0,buf,500);
    cout << write(1,buf,strlen(buf)) << endl;
 
    errno = 0;
    lseek(i,3,SEEK_SET);   //lseek(i,-3,SEEK_SET); //lseek(i,3,SEEK_END);
    if(errno != 0) cout << strerror(errno) << endl;
    write(i,buf,strlen(buf)-1);  //strlen(buf)-1表示去掉回车
    return 0;
  }
 
  编译执行程序4,运行结果是(info.txt文件中的内容是ABCDE):
 
  fd = 3
  File exists
  6      ABCDE
  abcde (键盘输入)
  abcde
  6
 
  使用命令cat info.txt查看文件中的内容是:ABCabcde。
  如果将程序中lseek语句换成“lseek(i,-3,SEEK_SET);”,重新编译执行。我们在键盘输入12345后,会提示Invalid argument。不过查看info.txt文件,会发现其中的内容已变成:ABCabcde12345。由此可知,负数不能与SEEK_SET一起使用,系统在提示错误的同时,会自动将写入的数据追加在文件末尾。
  把程序中的lseek语句再改成lseek(i,3,SEEK_END),重新编译执行。由键盘输入xyz后,查看文件内容是ABCabcde12345xyz,但会发现文件的大小由13变成19。使用命令od -c info.txt,会发现在xyz之前有三个\0。由此可知,在文件结尾处继续向后移动,如果写入数据则会自动在此前补入\0,这种文件被称为空洞文件。

posted on 2005-11-14 22:28  朱春雷  阅读(4955)  评论(0编辑  收藏  举报