Unix I/O
文件
在Linux里面,一切皆文件。每一个文件都有一个类型(type)来表明它在系统中的角色:
- 普通文件
(regular file)
包含任意数据 - 目录
(direction)相
关一组文件的索引 - 套接字
(socket)
和另一台机器上的进程通信的类型 - …
这篇博客主要介绍普通文件:
- 文本文件:
文本文件是只包含ASCII
或Unicode
字符的普通文件,就是一系列的文本行,每行以一个新行字符\n
结尾,其数字值是0xa
,和ASCII
码中的line feed
字符(LF)
一样。在Windows
和网络协议
之中是\r\n
(0xd 0xa)以结尾。 - 二进制文件
二进制文件则是除文本文件外的普通文件,如我们的程序、图片、视频等
打开文件
当我们想要读或写一个文件的时候,我们首先需要告诉系统内核,我们要访问到这个文件。内核(open函数
)则会返回一个小的非负整数(描述符
),我们后续的操作都得用到这个描述符。
在程序的一开始,已经默认打开了三个文件:
- 0: standard input(stdin)
- 1: standard output(stdout)
- 2: standar error(stderr)
1 |
|
参数介绍:
- filename
要打开或创建的目标文件的名字 - flags
打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行或
运算,构成falgs
。- O_RDONLY: 只读打开
- O_WRONLY: 只写打开
- O_RDWR: 可读可写
- O_CREAT: 若文件不存在,则创建它,需要使用mode选项。来指明新文件的访问权限
- O_APPEND: 追加写,如果文件已经有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容
- O_TRUNC: 如果文件存在,则截断它(删除已有的内容,重新写入)
- mode
mode参数描述了新文件的访问权限- S_IRUSR: 使用者(拥有者)能够读这个文件
- S_IWUSR: 使用者(拥有者)能够写这个文件
- S_IXUSR: 使用者(拥有者)能够执行这个文件
- S_IRGRP: 拥有者所在组的成员能够读这个文件
- S_IWGRP: 拥有者所在组的成员能够写这个文件
- S_IXGRP: 拥有者所在组的成员能够执行这个文件
- S_IROTH: 其他人(任何人)能够读这个文件
- S_IWOTH: 其他人(任何人)能够写这个文件
- S_IXOTH: 其他人(任何人)能够执行这个文件
关闭文件
1 |
|
- fd:要关闭的文件的描述符。
- 返回值:若成功返回0
- 出错则返回-1
注:当一个进程终止时,内核会自动关闭它所有打开的文件
读取文件
1 |
|
参数:
- fd:要读取的文件的描述符。
- buf:读取到的数据要放入的缓冲区。
- count:要读取的字节数。
返回值:
- 若成功返回读到的字节数,若已到文件结尾则返回0
- 若出错则返回-1并设置变量errno的值。
注:这里的size_t是无符号整型,ssize_t是有符号整型。
写入文件
1 |
|
参数:
- fd:文件描述符;
- buf:指定的缓冲区,即指针,指向一段内存单元;
- n:要写入文件指定的字节数;
返回值:
- 写入文档的字节数(成功);-1(出错)
write
函数把buf
中的n
个字节写入描述符
fd所指的文件,成功时返回写的字节数,错误时返回-1.
例子
1 |
|
这是一个比较简单的例子,把输入进终端的内容储存到文件abc.txt
里面。
下面是运行的结果:
1 | linux> ./test |
abc.txt
里面的内容为:
1 | test text |
重定向
在内核里,有三个相关的数据结构用于表示所打开的文件:
- 描述符表
每个进程都有自己的描述符表(Descriptor table),表项是由文件的描述符来索引 - 文件表
打开了的文件的集合,所有的进程都共享这一张表。表项包括文件的位置、引用的计数(reference count)(当前指向该表项的描述符数量)。直到这个数字变为了0,系统内核才会将其删除,否则不会。 v-node
表
所有的进程共享这张表,且每个表项包含stat结构的大多数信息。
下面看一个代码:
1 |
|
读取下面的这个文件abcde.txt
:
1 | abcde |
运行结果入下:
1 | linux> ./ffiles1 abcde.txt |
这是因为这里用到了一个dup2
函数,改变了fd3
描述符指向的文件,这也就是重定向的过程,如下图所示:
再来看一个例子:
1 |
|
运行结果入下:
1 | linux> ./ffiles3 abc.txt |
abc.txt
文件的内容入下:
1 | pqrswxyznef |
大致过程入下:
首先Write(fd1, "pqrs", 4);
把pqrs
写入到abc.txt
之中;
然后Write(fd3, "jklmn", 5);
把jklmn
写入到abc.txt
之中;
然后在原本fd1的指向之下,Write(fd2, "wxyz", 4);
把wxyz
写入到pqrs
的后面,把jklmn
的jklm
覆盖了,变成了wxyzn
;
最后Write(fd3, "ef", 2);
在wxyzn
后继续写入ef
。
所以abc.txt
的内容就是pqrswxyznef
。
最后再来看一个例子:
1 |
|
读取abcde.txt
的结果如下:
1 | linux> ./ffiles2 abcde.txt |
子进程把父进程的描述符表也复制了一份,但指向的仍然还是同一个文件表项,所以执行Read(fd1, &c2, 1);
的结果不一样。