系统级I/O

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

UNIX I/O

  • 所有的设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
  • 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  • Linux shell 创建的每个进程开始时都有三个打开的文件:
1
2
3
4
5

/* /usr/include/unisted.h */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */
  • 改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置 k, 是从文件开头起始的字节偏移量。
  • 读写文件。当读操作文件位置k大于文件大小时,触发 end-of-file(EOF) 的条件,应用程序能检测到这个条件。但是在文件结尾没有明确的“EOF”符号。
  • 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。

文件

文件类型

  • 普通文件 (regular file)包含任意数据 。分为文本文件 (text file) 和二进制文件 (binary file) , 对内核而言,文本文件和二进制文件没有区别。
  • 目录(directory)是包含一组链接 (link)的文件,其中每个链接都将一个文件名(filename) 映射到一个文件,这个文件可能是另一个目录。每个目录至少含有两个条目:“.”是到该目录自身的链接,以及”.. “是到目录层次结构中父目录的链接。
  • 套接字 (socket) 是用来与另一个进程进行跨网络通信的文件 。
  • 命名通道 (named pipe) 、符号链接 (symbolic link), 以及字符和块设备 (character and block device) ·······

Linux 内核将所有文件都组织成一个目录层次结构 (directory hierarchy) , 由名为/(斜杠)的根目录确定。系统中的每个文件都是根目录的直接或间接的后代。

image-20240227231054407

路径名可分为:绝对路径名(absolute pathname)相对路径名(relative pathname)

打开和关闭文件

1
2
3
4
5
6
7
#include <sys/types.h> 		//包含了一些系统数据类型的定义
#include <sys/stat.h> //包含了文件状态信息的相关定义,比如文件的权限、大小等。
#include <fcntl .h> //包含了文件控制相关的函数定义,比如打开文件、设置文件属性等。
#include <unistd.h>
int open(char *filename, int flags, mode_t mode); //返回进程中当前没有打开的最小描述符
int close(int fd);

读和写文件

1
2
3
4
5
6
7
8
9
#include <unistd.h> 

ssize_t read(int fd, void *buf, size_t n);
//返回:若成功则为读的字节数,若 EOF 则为0, 若出错为-1.

ssize_t write(int fd, const void *buf, size_t n);
//返回:若成功则为写的字节数,若出错则为-1.

//ssize_t为long

RIO包

  • 无缓冲的输入输出函数
  • 带缓冲的输入函数

//TODO

读取文件元数据

  • 通过调用statfstat函数检索元数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "csapp.h"

int main (int argc, char **argv)
{
struct stat stat;
char *type, *readok;

/* $end statcheck */
if (argc != 2) {
fprintf(stderr, "usage: %s <filename>\n", argv[0]);
exit(0);
}
/* $begin statcheck */
Stat(argv[1], &stat);
if (S_ISREG(stat.st_mode)) /* Determine file type */
type = "regular";
else if (S_ISDIR(stat.st_mode))
type = "directory";
else
type = "other";
if ((stat.st_mode & S_IRUSR)) /* Check read access */
readok = "yes";
else
readok = "no";

printf("type: %s, read: %s\n", type, readok);
exit(0);
}

读取目录内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "csapp.h"

int main(int argc, char **argv)
{
DIR *streamp;
struct dirent *dep;

/* $end readdir */
if (argc != 2) {
printf("usage: %s <pathname>\n", argv[0]);
exit(1);
}
/* $begin readdir */
streamp = Opendir(argv[1]); //成功返回指针,出错返回NULL

errno = 0;
while ((dep = readdir(streamp)) != NULL) { //成功返回下一个目录项的指针,没有更多目录项或出错返回-1
printf("Found file: %s\n", dep->d_name);
}
if (errno != 0)
unix_error("readdir error");

Closedir(streamp); //成功返回0,失败返回-1
exit(0);
}

共享文件

内核用三个数据结构描述打开的文件:

  • 描述符表 (descriptor table)。每个进程都有它独立的描述符表。
  • 文件表 (file table) 。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。内核不会删除这个文件表表项,直到它的引用计数为零。
  • v-node (v-node table) 同文件表一样,所有的进程共享。

image-20240401222347631

  • 当以同一个filename调用open两次

image-20240401222619054

  • 父子进程共享相同的打开文件表集合

image-20240401222638861

I/O重定向

  • >重定向操作符
  • dup2(int oldfd,int newfd)复制描述符表表项 oldfd 到描述符表表项 newfd, 覆盖描述符表表项 newfd 以前的内容。

image-20240401223531305

标准I/O

  • fopen()和fclose()
  • fread()和fwrite()
  • fgets()和fputs()

标准I/O将打开的文件模型化为一个流(相当于一个指向FILE类型的指针)

每个ANSI C 程序开始时都有三个打开的流stdin 、stdout和stderr, 分别对应于标准输入、标准输出和标准错误:

1
2
3
4
#include <stdio.h>
extern FILE *Stdin; // Standard input (descriptor 0)
extern FILE *Stdout; // Standard output (descriptor 1)
extern FILE *stderr; // Standard error (descriptor 2)