文件I/O

  • 所有执行I/O操作的系统调用都以文件描述符(一个非负整数)来指代打开的文件。包括pipe,FIFO,socket,终端,设备和普通文件。
  • 针对每一个进程,内核都会为其维护一个文件描述符表,每一个表项都是对打开文件句柄的引用+文件描述符标志二元组(不是文件状态标志,且目前为止只定义了一个标识就是close-on-exec)
  • 通过文件描述符在文件描述符表中找到相对应的引用,然后可以找到对应的文件表项;在文件表中记录了以下信息
1. 当前文件偏移量
2. 文件状态标志
3. 文件访问模式
4. 信号驱动IO相关
5. inode引用
6. v节点引用
  • inode中记录了以下信息
1. 文件访问权限和文件类型
2. 文件锁列表
3. 文件的各种属性(包括文件大小以及相关时间属性)
4. 数据块编号
  • v节点是用来支持虚拟文件系统的(VFS),能够为不同的文件系统提供统一的API,v节点中保存了各种底层函数调用的函数指针;此外,linux上没有特定的v节点的概念,但是本质上都是一样的,在linux下只有inode节点,所以相应的函数指针也都放在了i节点中
  • 两个不同的文件描述符,指向同一个文件表项,那么会共享同一个文件偏移量,这种情况通常是调用fork以后出现;通过dup或者fcntl复制文件描述符可以使两个fd指向相同的文件表项;通过两次open调用会使得两个不同的文件描述符指向不同的文件表项,但文件表项中的inode是相同的
  • 在打开文件时指定O_NONBLOCK或者调用fcntl设置非阻塞后,会出现以下两类情况
1. 若open调用未能立即打开文件,则返回错误而非陷入阻塞;但对于FIFO文件例外
2. 调用open成功后,对于后续的IO操作也都是非阻塞的,若IO操作不能立即完成,只能传输部分数据,调用失败,返回EAGAIN或者EWOULDBLCOK错误,两者等价
open
  • open函数调用的原型为int open(const char* pathname,int flags,.../*mode_t mode*/),如果pathname是一个符号链接(软链接),那么会对其解引用,即返回指向其实际文件的文件描述符。当flags没有指定O_CREAT标志,则忽略mode参数。如果open调用成功,返回当前进程文件描述符表中最小的未用的文件描述符;如果失败,返回-1并设置errno
  • flags表示文件状态标志,常见的关于文件读写权限的O_RDONLY\O_WRONLY\O_RDWR(三者互斥)
  • O_ASYNC标志表示对于返回的fd所指向的文件上有IO操作时,系统会产生一个信号通知应用程序,这仅对一些特殊的文件有效果,比如FIFO文件,socket文件以及终端设备文件。另外在linux中,需要调用fcntl的F_SETFL来设置此标志才起作用
  • O_NONBLOCK以非阻塞模式打开文件
  • O_TRUNC表示当文件存在且为普通文件,将文件长度截断为0
  • O_CLOEXEC为文件描述符启用close_on_exec,即调用exec函数族时,子进程不继承父进程打开的文件描述符;并且调用open并设置此标志为原子操作,另外可以调用fcntl的F_SETFD设置,但是此方法非线程安全
  • 当需要判断一个文件是否存在时,应当使用O_CREAT和O_EXCL标志来一次性调用open来判断(作为原子操作)
  • 当有多个进程向同一个文件尾部写入数据时,在没有同步的情况下如果使用lseek加write的方式,会出现竞争状态进而导致写入错误;正确的做法是使用O_APPEND标志的open,然后再write
read
  • read函数原型为ssize_t read(int fd,void* buffer,size_t count),返回值为一个有符号整形数,正常读返回读到的字节数,如果读到文件末尾返回0(EOF),如果调用出错返回-1;count表示最大读取的字节数
  • 如果读写位置位于文件尾部或者读取的文件为终端设备,管道,socket和FIFO,会出现读取的字节数小于请求字节数的情况;由于表示字符串终止的空字符的影响,缓冲区的大小至少要比最大读取字节数多一个字节
write
  • write函数原型为ssize_t write(int fd,void* buffer,size_t count)write调用可能会出现实际写入的字节小于指定的从buf写入文件的字节数,对于磁盘文件来说,可能是因为磁盘已经满了
  • write函数的参数与read基本一致,count表示要写入的字节数,成功调用返回写入的字节数,调用失败返回-1
lseek
  • lseek函数原型为off_t lseek(int fd,off_t offset,int whence)
  • offset表示偏移量,是一个有符号整数;分别表示正/负偏移
  • whence表示偏移地址,当设置为SEEK_SET时,偏移量必须为正数
  • lseek只是修改了fd对应文件表项中的文件偏移量的值,并没有去访问物理设备
  • 不允许将lseek应用于管道,socket,终端以及FIFO,如果这样调用了,调用将失败并将errno置为ESPIPE
  • 如果文件的文件偏移量已经超过了文件末尾,那么内核将以空字节去填充,不占用实际磁盘空间,这称为文件空洞。直到向文件空洞中去写入数据,内核才会真正去方位磁盘设备,申请磁盘空间
fcntl
  • 函数原型为int fcntl(int fd,int cmd,......)
  • 通过将cmd设置为F_GETFL可以获得fd所指向文件的文件状态标志,进而可以通过将其和各种状态标志做&操作,判断是否设置了该状态标志
  • 对于文件的访问权限来说,在判断的时候比较特殊;需要先将返回值与O_ACCMODE做&操作,然后才能进行判断,如下所示
int flags = fcntl(fd,F_GETFL);
accessmode = flags & O_ACCMODE;
switch(accessmode)
{
    case O_WRONLY:{...}
    case O_RDONLY:{...}
    case O_RDWR:{...}
}
  • 相反,可以使用F_SETFL来设置相应fd的文件状态标志;一般,都是先调用F_GETFL获取旧 的标志位,然后在旧的标志位的基础上对其进行修改;可以修改的状态标志为O_APPEND\O_NONBLOCK\O_NOATIME\O_ASYNC\O_DIRECT。不能通过F_SETFL修改文件的访问模式(O_RDONLY\O_WRONLY\O_RDWR)
  • 可以通过F_DUPFD复制文件描述符,格式为newfd = fcntl(oldfd,F_DUPFD,startfd),其中新创建的文件描述符将使用大于startfd的最小的且未用的文件描述符;如果想要为新fd设置close-on-exec标志,就附加F_DUPFD_CLOEXEC命令
dup
  • dup函数族中一共有三个函数,分别为dup,dup2,dup3。三个函数的原型如下
#include 

int dup(int oldfd);
int dup2(int oldfd,int newfd)
int dup3(int oldfd,int newfd,int flags)

return new fd on success,or -1 on error
  • dup仅仅复制一个已经打开的文件描述符,并返回新的文件描述符;dup2创建的新的fd是由newfd参数所指定的,如果newfd所指向的fd已经打开,那么dup2首先会将其关闭然后再完成复制;
  • dup和dup2所创建的文件描述符的fd标志(close-on-exec)都是处于关闭状态,而dup3在dup2的基础上可以控制该标志的开关
pread和pwrite
#include 

ssize_t pread(int fd,void* buf,size_t count,off_t offset);
                                          return nums of bytes read,0 on EOF,or -1 on error
ssize_t pwrite(int fd,const void* buf,size_t count,off_t offset);
                                          return nums of bytes written,or -1 on error
  • 为了避免竞争状态的产生,保证多进程或者多线程之间能够正确的在指定偏移处进行IO操作,可以使用这一组系统调用;在单线程环境下,pread调用等同于下面的调用过程(pwrite同理)。先保存了当前的文件偏移,然后对文件偏移进行修改并在修改后的偏移处进行read,最后读取完以后将偏移恢复
off_t curoffset = lseek(fd,0,SEEK_CUR);
lseek(fd,offset,SEEK_SET);
read(fd,buf,count);
lseek(fd,curoffset,SEEK_SET);
readv和writev
  • readv和writev都具有原子性,不被其他执行路径干扰(进程或线程)
#include 

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

  • 以上的系统调用可以一次投递多块缓冲区以供读写,iov是指向缓冲区结构数组的指针,iovcnt是数组中的缓冲区个数;struct iovec结构是一个用来描述缓冲区的结构体,定义如下
struct iovec{
      void* iov_base;
      size_t iov_len;
};
  • readv实现了分散输入的功能,从fd指定的文件中连续读取指定的字节数,然后按照顺序分别放入缓冲区数组中(从iov[0]开始)readv函数应用示例如下:
  1 #include "../sysHeader.h"
  2 
  3 int main(void)
  4 {
  5     struct iovec iov[3];
  6 
  7     int fd = open("temp",O_RDWR);
  8 
  9     if(fd == -1)
 10     {
 11         perror("open");
 12         exit(1);
 13     }
 14 
 15     char v1[64];
 16     char v2[64];
 17     char v3[128];
 18 
 19     iov[0].iov_base = v1;
 20     iov[0].iov_len = 64;
 21 
 22     iov[1].iov_base = v2;
 23     iov[1].iov_len = 64;
 24 
 25     iov[2].iov_base = v3;
 26     iov[2].iov_len = 128;
 27 
 28     int totalreq = 256;
 29 
 30     int res = readv(fd,iov,3);
 31 
 32     if(res < totalreq)
 33         printf("read fewer bytes than requered\n");
 34     else
 35         printf("v1 = %s\nv2 = %s\nv3 = %s\n",v1,v2,v3);
 36 
 37     return 0;
 38 }
truncate和ftruncate
       #include 
       #include 

       int truncate(const char *path, off_t length);
       int ftruncate(int fd, off_t length);
                          return 0 on success,or -1 on error
  • 若文件当前长度小于length,则在文件尾部添加空字节以增长到指定length;若文件当前长度大于length,则将文件长度减小至length

文件IO缓冲

  • 由于磁盘存取的速度比较缓慢,所以read\write调用并不等待实际的磁盘存取操作。内核为read/write准备了缓存,每当调用write写数据时,实际上是往缓存中写入,直到缓存满或者某个时机,系统会自动将缓存中的数据刷新到磁盘,这也减少了对磁盘的存取提高了效率。如果还没有将写入的数据刷新到磁盘上,且这时候有另外一个进程来读取该数据,内核会直接返回缓存中的数据,大大提高了效率;对于read调用也是类似的
  • 对于标准IO库stdio来说,里面的IO函数都是带缓冲的,可以使用setvbuf函数来控制stdio库使用缓冲的方式,参数stream表示要修改的某个文件流,mode指示了缓冲的类型,分别有三种控制类型:_IONBF/_IOLBF/_IOFBF。_IONBF表示不缓冲,相当于直接调用write和read;_IOLBF表示行缓冲,在遇到一个换行符之前缓冲数据,默认终端设备采用行缓冲;_IOFBF表示全缓冲,以buf和size指定的缓冲区为缓冲对象;参数buf表示缓冲区首地址,不能使用栈上的内存,因为随着函数返回,内存不可访问,size表示缓冲区大小。函数原型如下
       #include 

       int setvbuf(FILE *stream, char *buf, int mode, size_t size);
  • 无论使用何种缓冲方式,都可以使用fflush函数立即刷新stream流的缓冲区,如果stream为NULL则刷新所有的缓冲区,函数原型如下
       #include 

       int fflush(FILE *stream);
  • 对于内核缓冲区,我们也可以调用刷新函数来将缓冲区的内容刷新到磁盘,可以调用sync,fsync以及fdatasync三种刷新函数;linux实现的sync会将所有内核缓冲区中的数据排入写队列,且等待实际的写入完成才返回,而某些实现不等待IO完成就返回;fsync将与fd文件描述符相关的文件的所有信息都刷新到磁盘,并等待IO完成再返回;fdatasync只是将与文件描述符fd相关的文件的数据部分刷新到磁盘并等待完成后再返回;相应的函数原型如下
       #include 

       int fsync(int fd);
       int fdatasync(int fd);
       void sync(void);
  • 另外,在调用open时如果指定O_SYNC标志,会使得所有后续的输出同步,即等待数据(包括元数据)写入到磁盘上才返回
  • 文件描述符和标准库的流对象之间可以互相转换,函数原型如下
       #include 

       int fileno(FILE *stream);
       FILE *fdopen(int fd, const char *mode);

文件系统详述

  • 在linux中,一切都是文件,设备也是以文件的形式存在的;设备可分为字符设备文件和块设备文件,两者的区别就在于字符型设备基于每个字符来处理数据而块设备按照一块数据来处理数据。典型的,像鼠标就是字符设备而磁盘就属于块设备。应用程序对于各类设备的read/write调用实际上底层都是去调用了对应设备的驱动程序,最终完成实际的输入输出。
  • 以ext2文件系统为例,首先第一个部分是引导块,包含了引导操作系统的信息;然后剩余的空间被划分为大小相等的块组,每一个块组中包含了超级块,inode位图,数据块位图,inode表以及数据块表;超级块中记录了当前这个块组中的inode剩余量以及数据块的大小等信息;inode位图用来记录当前块组中inode节点的使用情况;数据块位图用来记录数据块的使用情况;而inode表和数据块表就是实际存放文件数据的地方,inode用来存放文件的元数据(包括文件大小,访问时间等),数据块用来存放文件的真正数据
  • 上面说过inode节点中存放了文件的元数据,维护了以下一些信息
  • 文件类型
  • 文件大小
  • 文件所属用户及用户组
  • 文件对应属组的权限位
  • 指向当前文件的硬链接数
  • 数据块指针,指向文件存放真正数据的数据块
  • 三个时间信息,对文件的最后访问时间,对文件的最后修改时间(对文件内容的修改)以及对文件的元数据的最后修改时间
  • 该文件所占有的数据块的数目
  • 对于inode节点中的数据块指针又分为四种类型,直接指针,一级指针,二级指针以及三级指针。在ext2中每个inode中包含15个与数据块相关的指针,其中前12个都是直接指针;而后面三个指针分别是一级,二级和三级指针。假设一个数据块为4096字节,那么对于文件大小小于12×4096字节的文件来说,只需要用到前12个指针,小文件的数据都直接放置于前12个指针指向的数据块中;如果超过这个大小,就需要用到一级指针了,一级指针指向的数据块中存放的不是数据,而是指向其他数据块的指针,而每个指针长度为4字节,则一共可以存放1024个指针,则在只使用13个指针的前提下,文件的大小最大可以使(1024+12)×4096字节;二级指针和三级指针类似。
  • 为了达到为所有文件系统提供同样的API的目的,linux在应用程序和具体的文件系统之间提供了一层抽象层,也就是VFS(虚拟文件系统);调用过程可能是这样,应用程序调用write,内核通过inode节点中的对应的函数指针找到对应文件系统的write调用,然后再去调用驱动程序完成写入

文件属性

  • 利用以下三种系统调用可以获取一个文件的元数据,一般都是从文件的inode节点中获取。文件属性保存于结构体stat中,stat会返回指定路径名的文件的文件属性信息;fstat返回指定文件描述符fd的文件属性;lstat与stat类似,区别在于如果指定路径是一个符号链接,那么返回的文件属性是与该符号链接相关,也就是不追踪,而stat会返回符号链接所指向的文件的信息。调用lstat和stat的进程不需要对指定的文件有任何权限,但是必须保证对pathname的父目录有执行权限;而fstat只需要提供一个有效的fd就可以。相关函数原型和结构体如下
       #include 
       #include 
       #include 

       struct stat {
               dev_t     st_dev;         /* ID of device containing file */
               ino_t     st_ino;         /* Inode number */
               mode_t    st_mode;        /* File type and mode */
               nlink_t   st_nlink;       /* Number of hard links */
               uid_t     st_uid;         /* User ID of owner */
               gid_t     st_gid;         /* Group ID of owner */
               dev_t     st_rdev;        /* Device ID (if special file) */
               off_t     st_size;        /* Total size, in bytes */
               blksize_t st_blksize;     /* Block size for filesystem I/O */
               blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */

               /* Since Linux 2.6, the kernel supports nanosecond
                  precision for the following timestamp fields.
                  For the details before Linux 2.6, see NOTES. */

               struct timespec st_atim;  /* Time of last access */
               struct timespec st_mtim;  /* Time of last modification */
               struct timespec st_ctim;  /* Time of last status change */

           #define st_atime st_atim.tv_sec      /* Backward compatibility */
           #define st_mtime st_mtim.tv_sec
           #define st_ctime st_ctim.tv_sec
           };

       int stat(const char *pathname, struct stat *statbuf);
       int fstat(int fd, struct stat *statbuf);
       int lstat(const char *pathname, struct stat *statbuf);
  • 对于返回的stat结构体的部分成员进行说明。st_ino表示该文件对应的inode编号;st_mode是一个16位长的字段,高4位表示文件类型,低12位表示文件权限。可以利用系统提供的宏来判断文件类型,例如S_ISREG()测试该文件是否是常规文件,具体参考man手册;st_nlink表示硬链接数;st_size表示文件大小;st_blocks表示分配给该文件的数据块数目;st_blksize表示在该文件上进行IO操作时所采用的最优缓冲区大小
  • 每一个文件都有一个相关联的用户和组ID,分别用st_uid和st_gid表示。在文件创建时,用户和组ID分别取自进程的有效用户和组ID
  • 对于文件的权限位来说,可以用st_mode和各种权限位相&的方式来判断,具体见man手册;另外,可以通过filePermStr调用将st_mode转换为一个形如"rwxr-xr--"的字符串,flags参数指定为FP_SPECIAL,原型为char* filePermStr(mode_t perm,int flags),位于sys/types.h头文件中
  • 对于目录来说,读写执行权限有不同的意义。目录具有读权限表示只能查看目录下的文件名;具有写权限可以对目录下的文件进行修改或者增删文件;具有执行权限可以进入该目录
  • 当进程访问一个文件时,会依次根据进程的有效用户ID,有效组ID以及附属组ID来分别检查对于该文件的权限;
  • 调用chown可以改变一个文件的属主和属组。只有对于特权级进程来说,才能更改文件的属主;非特权级进程只有在进程的有效用户ID和文件当前属主一致的情况下才能改变文件的属主。函数原型为int chown(const char* pathname,uid_t owner,gid_t group)
  • 若想要更改文件的权限位,一般先调用stat获取文件的原权限,然后在此基础上进行修改,最后调用chmod。同样的,对于chmod来说有两种情况。当调用进程是特权级进程,可以修改文件的权限位;当调用进程是非特权进程,其有效用户ID必须和文件属主一致。函数原型为int chmod(const char* pathname,mode_t mode)
  • 注意到文件名并没有包括在inode节点的文件属性中。文件名是存放在目录下的,对于一个目录来说,它的inode的文件类型表示一个目录,并且inode中数据块指针所指向的数据块中存放的是关于文件名和inode的映射。有可能存在多个不同的文件名指向相同的一个inode,这就是一个硬链接。如果一个inode节点的硬链接计数为0,那么该文件就会被删除,即释放相关inode和数据块
  • 硬链接存在两个问题,其一是只能应用于同一个文件系统下,因为inode编号的唯一性不能跨文件系统;其二是不能为目录创建硬链接,避免出现链接环路
  • 为解决上述问题,就要用到符号链接。建立一个符号链接相当于建立一个新的文件,也就新建了一个inode节点,符号链接的文件数据只包括所引用文件的绝对路径,当系统发现这是一个符号链接时,会自动解引用找到真正的文件
  • 有关硬链接的函数原型如下,link和unlink分别是创建和删除一个硬链接,需要注意的是link和unlink不会对pathname解引用,即如果oldpath或者pathname是符号链接,那么只会针对该符号链接去新建或者删除硬链接,而不涉及引用的实际文件
       #include 

       int link(const char *oldpath, const char *newpath);
       int unlink(const char *pathname);
  • rename调用同shell命令mv,可以移动或者重命名一个文件,原型为int rename(const char* oldpath,const char* newpath)
  • symlink调用用于创建一个符号链接,如果target所代表的文件不存在,那么linkpath成为悬空链接,原型为int symlink(const char *target, const char *linkpath);另外,解除一个符号链接也是调用unlink函数。如果用open去打开一个符号链接,系统会为其解引用,即打开symbol link所引用的文件。所以如果想要查看符号链接本身的内容,那么就使用readlink函数,buffer是保存内容的缓冲区,bufsiz表示缓冲区大小,pathname表示符号链接路径,原型为ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);需要注意的是,这个函数的返回值为放入buf中的字节数,并且buf中不会包括空字符,所以一般会分配一个长度为路径最大长度的缓冲区,示例如下(解析一个符号链接的内容)
  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #include 
  6 
  7 #define BUFFSIZE PATH_MAX
  8 
  9 int main(void)
 10 {
 11     char buf[BUFFSIZE];
 12     char* path = "lntag";
 13     struct stat st;
 14 
 15     int res = lstat(path,&st);
 16     if(res == -1)
 17     {
 18         perror("stat");
 19         exit(1);
 20     }
 21 
 22     if(!S_ISLNK(st.st_mode))
 23     {
 24         printf("lntag is not a slink\n");
 25         exit(1);
 26     }
 27 
 28     int nums = readlink(path,buf,BUFFSIZE - 1);
 29     buf[nums] = '\0';
 30     printf("lntag:%s\n",buf);
 31     
 32     return 0;
 33 }
  • 对于目录的访问不能使用open函数,调用open会出错;对于目录的访问需要另外一组API函数,首先需要使用opendir或者fdopendir获得对目录的一个指针,该指针是DIR类型的。然后循环调用readdir,传入目录指针,该函数的返回值是一个指向dirent结构体的指针,这个结构体相当于一条目录项,存放文件的inode和文件的名字以及其他信息;并且每调用一次readdir,就会自动从指定的目录指针中获取下一条目录项,直到返回NULL表示遍历目录结束。还可以调用rewinddir函数将目录指针重新指向目录初始处,另外遍历结束后应调用closedir释放资源。相关函数原型及结构体如下
       #include 
       #include 

       struct dirent {
               ino_t          d_ino;       /* Inode number */
               off_t          d_off;       /* Not an offset; see below */
               unsigned short d_reclen;    /* Length of this record */
               unsigned char  d_type;      /* Type of file; not supported
                                              by all filesystem types */
               char           d_name[256]; /* Null-terminated filename */
           };

       DIR *opendir(const char *name);
       DIR *fdopendir(int fd);
       struct dirent *readdir(DIR *dirp);
       void rewinddir(DIR* dirp);
       int closedir(DIR* dirp);
  • 可以通过dirfd函数获得对应于目录流的文件描述符,原型为int dirfd(DIR* dirp)
  • 关于进程的当前工作目录,可以调用getcwd函数来获取,该函数将当前工作目录字符串存放于cwdbuf数组中,size为函数写入cwdbuf数组的最大长度,如果调用成功返回一个指向cwdbuf的指针;另外可以调用chdir和fchdir来改变当前工作目录,相关函数原型如下
       #include 

       int chdir(const char *path);
       int fchdir(int fd);
       char *getcwd(char *buf, size_t size);
  • dirname和basename函数分别获取对于pathname的目录和文件名,例如对于路径"/home/pty/file.c",dirname返回"/home/pty",basename返回"pty",函数原型如下
#include 

char* dirname(char* pathname);
char* basename(char* pathname);

你可能感兴趣的:(文件I/O)