本文内容是csapp一书第十章的笔记和代码讲解
10.1Unix I/O
首先来讲一讲一句经典的语录:一切皆文件
在 Linux 中,这是一句很经典的话,自然我们所有的I/O设备也都是文件,而所有的输入和输出都被当作对相应的文件的读和写,不得说这是真是很优雅的方式。
而Unix I/O是什么呢?
我们都知道printf和scanf,他们是ANSI C提供的标准I/O库,可以这么说他们是比较高层的I/O函数。而在linux中是通过Unix I/O函数来实现这些高层函数的,其实就是printf等的源码的意思。举个例子,其中有 open() 和 close() 来打开和关闭文件, read() 和 write() 来读写文件,利用 lseek() 来设定读取的偏移量等等。
10.2文件
每个Linux文件都有一个类型来表明它在系统中的角色,主要有:
- 普通文件(regular file):包含任意数据,如文本文件(只有ASCLL或Unicode字符的普通文件)和二进制文件(除文本文件外所有)
- 目录(directory):形象的说就是文件夹
- 套接字:用于跨网络通信文件
下面是linux目录结构图,根目录为:/ 。
所有用户对应主目录都在home下。
10.3打开和关闭文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode);
参数解析
返回值:open函数会返回一个文件描述符,是在当前进程中没有打开的最小描述符
filename:一个文件路径,可以是相对路径,也可以是绝对路径
flags:
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:可读可写
O_CREAT: 文件不存在,就创建一个。
O_TRUNC: 如果文件已经存在,就截断它。这里用词是截断而不是清空,后面我们会知道为什么
O_APPEND:每次写操作,都在文件结尾处添加
mode参数
主要是一些用户访问权限信息,可以根据不同的权限信息的“&”和“|”操作,来赋予要给文件不同权限。这里只举一些例子:
S_IRUSR:使用者(拥有者)能够读这个文件
S_IWUSR:使用者(拥有者)能够写这个文件
S_IXUSR:使用者(拥有者)能够执行这个文件
int close(int fd);
参数: fd 需要关闭文件的文件描述符
10.4读和写文件
ssize_t read(int fd,void *buf,size_t n);
ssize_t write(int fd,const void *buf,size_t n);
参数:从描述符为 fd 的当前文件位置复制最多 n 个字节 到 内存位置 buf 返回:若成功则为读的字节数,若为EOF则为0,若出错为-1 。
需要注意的是返回值的类型ssize_t和参数类型size_t的不同,实际上返回值由于需要返回-1导致其范围比size_t小了一倍
下面是一个例程,实现了我们在键盘输入什么就在显示器输出什么。
具体过程为:从标准输入就是键盘当中输入一个字符的时候,就读取一个字符到缓冲区中去,直到读到一个\n就输出所有缓冲区的内容。
这里也体现了一切皆文件的。STDIN_FILENO和STDOUT_FILENO为标准输入流(键盘等)和标准输出流(显示器等)。其中Read(STDIN_FILENO, &c, 1) != 0类似于getchar
/* $begin cpstdin */
#include "csapp.h"
int main(void)
{
char c;
while(Read(STDIN_FILENO, &c, 1) != 0)
Write(STDOUT_FILENO, &c, 1);
exit(0);
}
结果
zzz@ubuntu:/mnt/hgfs/shared$ ./a.out
abcd
abcd
**10.6读取文件元数据**
什么是元数据?
元数据是用来描述数据的数据,由内核维护,可以通过 stat 和 fstat 函数来访问,
```c
#include <unistd.h>
#include <sys/stat.h>
int stat(const char*filename,struct stat* buf);
int fstat(int fd,struct stat* buf);
stat的数据结构
struct stat
{
dev_t st_dev; // Device
ino_t st_ino; // inode
mode_t st_mode; // Protection & file type
nlink_t st_nlink; // Number of hard links
uid_t st_uid; // User ID of owner
gid_t st_gid; // Group ID of owner
dev_t st_rdev; // Device type (if inode device)
off_t st_size; // Total size, in bytes
unsigned long st_blksize; // Blocksize for filesystem I/O
unsigned long st_blocks; // Number of blocks allocated
time_t st_atime; // Time of last access
time_t st_mtime; // Time of last modification
time_t st_ctime; // Time of last change
}
这里讲一个成员:st-mode其他的大家可自行谷歌百度
st-mode代表文件类型:
S_ISREG(m)是否为普通文件
S_ISDIR(m)是否为目录文件
S_ISSOCK(m)是否为网络套接字
10.8共享文件 和 10.9重定向
这是一个挺重要的话题,我们首先很容易想到一个文件是可以被打开多次的,这会引发一系列的问题,都需要共享文件这个概念来解答。
首先内核用三个相关的数据结构来表示打开的文件
- 描述符表 每个进程都有独立的描述符表,表项是由进程打开的文件描述符来索引的。每个描述符表项指向文件表中的一个表项。
- 文件表 打开文件的集合是由一张文件表来表示的,所有进程共享。它记录了当前文件的位置(指向同一个文件表则共享文件位置),当前指向该表项的描述符表项数(成为引用计数)和一个指向v-node表中对应表项的指针。当引用计数为0是,内核会自动删除这个文件表表项。
- v-node表。所有进程共享,包含了stat结构中的大多数信息。
这里借用不周山系列几张图来解释这些概念,原文网址:https://wdxtub.com/csapp/thin-csapp-6/2016/04/16/
1.打开两个文件A和B,可以看到fd1指向文件A(之前说过fd1 fd2 fd3是固定的文件描述符,所以这个是标准输出流文件),而fd4应该就是程序员后面自己又open的一个文件了。
2.fork的影响
我之前讲fork的博客讲到过,子进程继承父进程一切,所以描述符表自然也继承了,所以生成了一个和之前描述符表一摸一样的表,注意文件A和B的引用次数+1,这和这篇文章以前讲的 当引用计数为0是,内核会自动删除这个文件表表项是吻合的。
了解了上面的概念下面介绍一下重定向:
顾名思义,重定向就是改变fd指向的文件表,对应第一张图对fd1重定向后:
重定向使用函数
int dup2(int oldfd, int newfd);
这个函数就是复制oldfd到newfd,特别要注意顺序哦
最后结合两个我们考试题目落到实践
1.
#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;
}
这个很常规,画画图就出来了
输出结果顺序为 a a b
一开始fd1,fd2,fd3拥有不同的打开文件表接着,然后dup2让fd2指向了fd3指向的打开文件表。我之前写概念就特意提及,打开文件表中拥有文件位置信息。所以dup2后fd2和fd3实际上是共享文件位置的,也就是共享我们在window操作系统下看到的光标的位置。剩下的就很好理解了
2
#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;
}
这道题考试时纠结了很久,脑海主要的问题就是对O_TRUNC的争议,还好最后对了:) 5分啊!
我之前讲概念是也特意为这里铺垫了一些,我说这个中文翻译是截断,而不是清空是有他的道理的,我考试时也是受到截断这个词的启发。
废话不多说开始分析:
这里有一个新的函数dup
int dup(int oldfd);
考试的时候其实是猜的,猜是由oldfd复制一个fd,事实上也是这样。
解题过程:再从头开始看,一开始通过fd1写入pqrs,此时fd1指向的打开文件表的文件位置指向第四个字母后面。然后通过fd3最加(通过参数O_APPEND知道是最加)jklmn,记住此时fd3的光标位置。然后通过复制fd1的fd2再写入wxyz,既然是复制,自然也是带O_TRUNC的,所以会有截断发生,wxyz被截断而前面的被保留。
这也是为什么被翻译成截断而不是清空,因为按照正常的情况,我们open一个文件,此时fd一定在文件开头,这时的截断效果看上去和清空一样,但这是很具有误导性的称呼,我们在这道题看到O_TRUNC和清空完全不同,详细地说效果是从光标处开始覆盖后面地内容,没有被覆盖的内容则被清空。啰嗦地讲了这么多,本道题已经迎刃而解了,最后答案是pqrswxyzef。