第十章 系统级I/O


Unix I/O


输入/输出(I/O)是在主存和外部设备(例如磁盘驱动器、终端和网络)之间赋值数据的过程。

  • 输入是从I/O设备复制数据到主存
  • 输出是从主存复制数据到I/O设备

一个Linux文件就是一个m个字节的序列, 所有的I/O设备都被模型化为文件 ,而所有的输入和输出都被当作对相应文件的读和写来执行。Unix I/O使得所有的输入输出都以一种统一且一致的方式来执行:

  • 打开文件:程序通过要求内核打开相应文件,宣告它要访问一个I/O设备,内核返回一个小的非负整数,叫做 描述符 ,它在后续对此文件的所有操作中标识这个文件。内核记录有关打开文件的所有信息,应用程序只需记住这个描述符。Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。
  • 改变当前文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,表示从文件开头起始的字节偏移量,应用程序可以通过seek操作显式设置文件的当前位置k。
  • 读写文件:读操作是从文件复制n>0个字节到内存,当前文件位置从k开始增加到k+n;写操作是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  • 关闭文件:内核释放文件打开时创建的数据结构,并将描述符恢复到可用的描述符池中。

文件


Linux文件都有一个类型:

  • 普通文件:包含任意数据,文本文件只含有ASCII或Unicode字符,二进制文件是所有其他的文件,其中文本文件每一行都是一个字符序列且以\n结尾,其数值为0x0a
  • 目录:包含一组链接的文件,每个链接都将一个文件名映射到一个文件,每个目录至少包含“.”(该目录自身链接)和“..”(父目录链接)条目。可用mkdir创建一个目录,ls查看目录内容,rmdir删除目录。
  • 套接字(socket):用来与另一个进程进行跨网络通信的文件。
  • 其他文件类型:包括命名通道、符号链接、字符和块设备等。

打开和关闭文件


进程通过调用open函数打开一个已存在的文件或者创建一个新文件。
open函数将filename转换为文件描述符,返回的 描述符总是进程中当前没有打开的最小描述符
flags参数指明进程如何访问文件:

  • O_RDONLY:只读
  • O_WRONLY:只写
  • O_RDWR:可读可写
  • O_CREAT:文件不存在则创建
  • O_TRUNC:文件已存在则截断
  • O_APPEND:写文件时设置文件位置到文件结尾

mode参数指定新文件的访问权限位。

#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
// 成功时返回文件描述符,出错返回-1  
int open(char *filename, int flags, mode_t mode);  

进程通过调用close函数关闭一个打开的文件。
关闭一个已关闭的描述符会出错。

#include <unistd.h>  
// 成功返回0,出错返回-1  
// fd参数表示文件描述符  
int close(int fd);  

读和写文件


应用程序通过调用readwrite函数执行输入输出。
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

#include <unistd.h>  
// 成功时返回读的字节数,若EOF则为0,出错返回-1  
ssize_t read(int fd, void *buf, size_t n);  
// 成功时返回写的字节数,出错返回-1  
ssize_t write(int fd, const void *buf, size_t n);  

x84-64系统中,size_t被定义为unsigned long,而ssize_t被定义为long

RIO(Robust I/O)包:自动处理读写过程中的信号中断,反复调用readwrite处理不足值,直到传送完所有请求数据。

读取文件元数据


程序通过调用statfstat函数,检索到关于文件的信息/元数据(metadata)。
stat函数以文件名作为输入,并将相关文件元数据写入stat结构体中。
fstat函数与stat函数功能类似,只是其以文件描述符作为参数。
stat结构体中有两个常用的成员:

  • st_mode:编码了文件访问许可位和文件类型
  • st_size:文件的字节数大小
#include <unistd.h>  
#include <sys/stat.h>  
// 成功时返回0,出错返回-1  
int stat(const char *filename, struct stat *buf);  
int fstat(int fd, struct stat *buf);  

读取目录内容


应用程序通过readdir系列函数读取目录内容。
目录流是对条目有序列表的抽象,每次调用readdir返回指向流中下一个目录项的指针,没有更多目录项或出错时返回NULL。每个目录项的结构为如下,d_ino是文件位置,d_name是文件名。

struct dirent  
{  
    ino_t d_ino;   // inode number  
    char d_name[256];  // filename  
};  

检查readdir是否错误的方法是检查调用readdir以来errno是否被修改过。

#include <sys/types.h>  
#include <dirent.h>  
// 成功时返回指向目录流的指针,出错返回NULL  
DIR *opendir(const char *name);  
// 成功时返回指向下一个目录项的指针,出错则返回NULL  
struct dirent *readdir(DIR *dirp);  

函数closedir关闭流并释放其所有资源。

#include <dirent.h>  
// 成功时返回0,错误返回-1  
int closedir(DIR *dirp);  

共享文件


内核用三个相关的数据结构表示打开的文件:

  1. 描述符表 :每个进程有独立的描述符表,表项是由进程打开的文件描述符来索引的,每个打开的描述符表项指向文件表中的一个表项。
  2. 文件表 :打开文件的集合是由一张文件表表示的,所有进程共享这张表,每个文件表表项包括当前文件位置、引用计数(指向该表项的描述符表项数)以及指向v-node表中对应表项的指针。
  3. v-node表 :所有进程共享一张v-node表,每个表项包含stat结构中的大多数信息,包括st_mode和st_size等。

父进程调用fork()时,子进程有一个父进程描述符表的副本, 父子进程共享相同的打开文件表集合 ,因此共享相同的文件位置。在内核中删除相应文件表表项之前,父子进程必须都关闭了它们的描述符。

I/O重定向


I/O重定向操作符允许用户将磁盘文件和标准输入和输出系统联系起来。
ls > foo.txt表示把执行ls得到的标准输出重定向到磁盘文件foo.txt。
使用dup2函数可以进行I/O重定向,dup2函数赋值描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。
dup2(4,1)可以使得任何写到标准输出的数据都被重定向到文件描述符为4的文件。

#include <unistd.h>  
// 成功时返回非负的描述符,出错返回-1  
int dup2(int oldfd, int new fd);  

RIO包中的示例


使用带缓冲区的读时,只要缓冲区中还有未读的字节,那么接下来再进行读时可以直接从流缓冲区中得到服务。

#define RIO_BUFSIZE 8192  
typedef struct  
{  
    int rio_fd;                /* Descriptor for this internal buf */  
    int rio_cnt;               /* Unread bytes in internal buf */  
    char *rio_bufptr;          /* Next unread byte in internal buf */  
    char rio_buf[RIO_BUFSIZE]; /* Internal buffer */  
}rio_t;  
// 从缓冲区复制n和rp->rio_cnt中较小值个字节到用户缓冲区,返回复制的字节数  
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)  
{  
    int cnt;  

    while(rp->rio_cnt <= 0)  /* Refill if buf is empty */  
    {  
        rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf));  
        if(rp->rio_cnt < 0)  
        {  
            if(errno != EINTR)  /* Interrupted by sig handler return */  
                return -1;  
        }  
        else if(rp->rio_cnt == 0)  /* EOF */  
            return 0;  
        else  
            rp->rio_bufptr = rp->rio_buf;  /* Reset buffer ptr */  
    }  

    /* Copy min(n, rp->rio_cnt) bytes from internal buf to user buf */  
    cnt = n;  
    if(rp->rio_cnt < n)  
        cnt = rp->rio_cnt;  
    memcpy(usrbuf, rp->rio_bufptr, cnt);  
    rp->rio_bufptr += cnt;  
    rp->rio_cnt -= cnt;  
    return cnt;  
}  

应该使用哪些I/O函数


对于大多数应用程序而言,标准I/O更简单,是优于Unix I/O的选择,但是Unix I/O比标准I/O更适用于网络应用程序:

  1. 只要有可能就使用标准I/O:对磁盘和终端设备I/O来说,标准I/O函数是首选,比如fopen,fclose,fread,fwrite,fgets,fputs,scanf,printf
  2. 不要使用scanfrio_readlineb读二进制文件,这些函数是用来读取文本文件的,二进制文件可能散布很多0xa字节,而这些字节又与终止文本行无关。
  3. 对网络套接字的I/O使用健壮的RIO函数,应用进程通过读写套接字描述符来与运行在其他计算机的进程实现通信。

评论

还没有登陆?评论请先登陆注册

还没有评论,抢个沙发吧!

 联系方式 contact me

Github
Email
QQ
Weibo