0%

系统级IO与四个函数

Unix I/O

文件

在Linux里面,一切皆文件。每一个文件都有一个类型(type)来表明它在系统中的角色:

  • 普通文件(regular file)包含任意数据
  • 目录(direction)相关一组文件的索引
  • 套接字(socket)和另一台机器上的进程通信的类型

这篇博客主要介绍普通文件:

  • 文本文件:
    文本文件是只包含ASCIIUnicode字符的普通文件,就是一系列的文本行,每行以一个新行字符\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
2
3
4
5
#include<sys/types.h>
#incldue<sys/stat.h>
#include<fcntl.h>

int open(char *filename,int flags,mode_t mode);

参数介绍:

  • 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
2
3
#include <unistd.h>

int close(int fd);
  • fd:要关闭的文件的描述符。
  • 返回值:若成功返回0
  • 出错则返回-1

注:当一个进程终止时,内核会自动关闭它所有打开的文件

读取文件

1
2
3
#include <unistd.h>

ssize_t read (int fd, void *buf, size_t n);

参数:

  • fd:要读取的文件的描述符。
  • buf:读取到的数据要放入的缓冲区。
  • count:要读取的字节数。

返回值:

  • 若成功返回读到的字节数,若已到文件结尾则返回0
  • 若出错则返回-1并设置变量errno的值。

注:这里的size_t是无符号整型,ssize_t是有符号整型。

写入文件

1
2
3
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t n);

参数:

  • fd:文件描述符;
  • buf:指定的缓冲区,即指针,指向一段内存单元;
  • n:要写入文件指定的字节数;

返回值:

  • 写入文档的字节数(成功);-1(出错)

write函数把buf中的n个字节写入描述符fd所指的文件,成功时返回写的字节数,错误时返回-1.

例子

1
2
3
4
5
6
7
8
9
10
11
#include "csapp.h"

int main(void)
{
char c;
int fd = open("abc.txt",O_WRONLY|O_CREAT|O_TRUNC);
while(read(STDIN_FILENO, &c, 1) != 0)
write(fd, &c, 1);
close(fd);
exit(0);
}

这是一个比较简单的例子,把输入进终端的内容储存到文件abc.txt里面。
下面是运行的结果:

1
2
3
linux> ./test
test text
^C

abc.txt里面的内容为:

1
2
3
test text

注:上面还有一个换行符(\n)没显示出来

重定向

在内核里,有三个相关的数据结构用于表示所打开的文件:

  • 描述符表
    每个进程都有自己的描述符表(Descriptor table),表项是由文件的描述符来索引
  • 文件表
    打开了的文件的集合,所有的进程都共享这一张表。表项包括文件的位置、引用的计数(reference count)(当前指向该表项的描述符数量)。直到这个数字变为了0,系统内核才会将其删除,否则不会。
  • v-node
    所有的进程共享这张表,且每个表项包含stat结构的大多数信息。


下面看一个代码:

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

int main(int argc, char *argv[])
{
int fd1, fd2, fd3;
char c1, c2, c3;
char *fname = argv[1];
fd1 = Open(fname, O_RDONLY, 0);
fd2 = Open(fname, O_RDONLY, 0);
fd3 = Open(fname, O_RDONLY, 0);
dup2(fd2, fd3);

Read(fd1, &c1, 1);
Read(fd2, &c2, 1);
Read(fd3, &c3, 1);
printf("c1 = %c, c2 = %c, c3 = %c\n", c1, c2, c3);

Close(fd1);
Close(fd2);
Close(fd3);
return 0;
}

读取下面的这个文件abcde.txt

1
abcde

运行结果入下:

1
2
linux> ./ffiles1 abcde.txt
c1 = a, c2 = a, c3 = b

这是因为这里用到了一个dup2函数,改变了fd3描述符指向的文件,这也就是重定向的过程,如下图所示:

再来看一个例子:

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

int main(int argc, char *argv[])
{
int fd1, fd2, fd3;
char *fname = argv[1];
fd1 = Open(fname, O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR);
Write(fd1, "pqrs", 4);

fd3 = Open(fname, O_APPEND|O_WRONLY, 0);
Write(fd3, "jklmn", 5);
fd2 = dup(fd1); /* Allocates new descriptor */
Write(fd2, "wxyz", 4);
Write(fd3, "ef", 2);

Close(fd1);
Close(fd2);
Close(fd3);
return 0;
}

运行结果入下:

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的后面,把jklmnjklm覆盖了,变成了wxyzn
最后Write(fd3, "ef", 2);wxyzn后继续写入ef
所以abc.txt的内容就是pqrswxyznef

最后再来看一个例子:

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

int main(int argc, char *argv[])
{
int fd1;
int s = getpid() & 0x1;
char c1, c2;
char *fname = argv[1];
fd1 = Open(fname, O_RDONLY, 0);
Read(fd1, &c1, 1);
if (fork()) {
int status;
wait(&status);
Read(fd1, &c2, 1);
printf("Parent: c1 = %c, c2 = %c\n", c1, c2);
} else {
/* Child */
Read(fd1, &c2, 1);
printf("Child: c1 = %c, c2 = %c\n", c1, c2);
}
return 0;
}

读取abcde.txt的结果如下:

1
2
3
linux> ./ffiles2 abcde.txt
Child: c1 = a, c2 = b
Parent: c1 = a, c2 = c


子进程把父进程的描述符表也复制了一份,但指向的仍然还是同一个文件表项,所以执行Read(fd1, &c2, 1);的结果不一样。


参考资料