系统级IO

主要参考CSAPP

简介

所有语言运行时系统都提供执行IO较高级别工具。例如,ANSI C提供了标准IO库,包含像printf和scanf这样执行带缓冲区的IO函数。C++重载了 >> 和 << 提供类似功能。在Linux系统中,使用是通过内核提供的系统级Unix IO函数来实现这些较高级别的IO函数。

Unix IO

一个Linux文件就是一个m个字节的序列,所有的IO设备都没模型话为文件,而所有的输入和输出都被当作相应文件的读写和执行。通过这种方式,所有的输入和输出都能以一种统一且致的方式来执行。

  • 打开文件:一个应用程序通过要求内核打开相应文件,来宣告它想要访问一个IO设备。内核返回一个非负整数,叫做描述符,它在后续对此文件的所有操作中标示这个文件。内核记录有关这个打开文件所有信息。应用程序只需要记住这个描述符
  • Linux shell创建的每个进程开始时都有三个打开文件:标准输入(描述符为0),标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO, STDOUT_FILENO 和 STDERROR_FILENO。他可以用来代替显示的描述符。
  • 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k。初始为0.这个文件位置是从文件开头其实的字节偏移量。应用程序能够通过执行seek操作,显示地设置文件的当前位置k。
  • 读写文件:一个读操作就是从文件复制 n > 0个字节到内存,从当前文件位置k开始,然后将k增加到k + n。给定一个大小为m字节的文件,当k >= m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的”EOF符号”
  • 关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的资源。

打开和关闭文件

进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的:

#incldue <sys/types.h>
#incldue <sys/stat.h>
#incldue <fcntl.h>

int open(char *filename, int flags, mode_t mode);
复制代码

open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:

  • O_RDONLY: 只读
  • O_WRONLY: 只写
  • O_RDWR: 可读可写

例如,下面的代码说明如何以读的方式打开一个已存在的文件

fd = Open("foo.txt", O_RDONLY, 0);
复制代码

flags参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:

  • O_CREAT: 如果文件不存在,就创建一个截断的文件。
  • O_TRUNC: 如果文件已经存在,就截断它
  • O_APPEND: 每次写操作之前,设置文件位置到文件的结尾处。

例如,下面代码说明是如何打开一个已存在的文件,后面添加一些数据:

fd = open("foo.txt", O_WRONLY | O_APPEND, 0);
复制代码

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

作为上下文的一部分,每个进程都有一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open参数调用来创建一个新文件,文件的访问权限位被设置为mode & ~ umask。例如,假设我们给定下面的mode 和 umask的值:

#define DEF_MODE S_IRSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK S_IWGRP|S_IWOTH
复制代码

接下来,下面的代码片段创建一个新文件,文件的拥有者有读写权限,而所有其他的用户都有读权限:

umask(DEF_UMASK);
fd = Open("foo.txt", O_CREAT|O_TEUNC|O_WEONLY, DEF_MODE);  
复制代码

最后,进程通过调用close函数关闭一个文件

#include <unistd.h>
int close(int fd);
复制代码

如下:

int fd1, fd2;

fd1 = Open("foo.txt", O_RDONLY, 0);
Close(fd1);

fd2 = Open("foo.txt", O_RDONLY, 0);
printf("fd2 = %d \n", fd2);


exit(0);
复制代码

读和写文件

应用程序是通过分别调用read和write函数来执行输入和输出的。

#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);
复制代码

read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。

write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

例如,一个程序用read和write调用一次一个字节从标准输入复制到标准输出

char c;

while (Read(STDIN_FILENO, &c, 1) != 0) {
  Write(STDOUT_FILENO, &c, 1);
}

exit(0);
复制代码

这时候就可以在输入栏里面输入,确认回车后控制台就会输出所输入的。

hello world unix io
hello world unix io
复制代码

ssize_t和size_t有什么区别

read函数有一个size_t的输入参数和一个ssize_t的返回值。那么这两种方式有什么区别呢?在x86-64系统中,size_t被定义为unsigned long,而ssize_t被定义为long。因为read会出错,会返回-1所有要用long类型来表示

在某些情况下,read和write传送的字节比应用程序要求的要少。这些不足值不表示有错误。出现这样的情况原因有:

  • 读时遇到EOF。当我们的缓冲区较大而文件字节数较少时,会有这样的问题
  • 从终端读文本行。如果打开文件是和终端关联的,那么read将一次传送一个文本行,返回的不足值等于文本行大小
  • 读和写网络套接字。如果打开的文件对应于网络套接字,那么内部缓冲约束和较长的网络延时会引起read和write返回不足值。对Linux管道调用read和write时,也可能出现不足值。

实际上,除了EOF,当你读磁盘文件时,将不会遇到不足值,而且在写磁盘文件时,也不会遇到不足值。然后,如果你想创建健壮的诸如Web服务器这样的网络应用,就必须通过反复调用read和write处理不足值,直到所需要的字节都传送完毕

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享