系统级I/O
打开和关闭文件
读和写文件
读取文件元数据
共享文件
I/O重定向
所有的I/O设备(例如网络,磁盘和终端)都被模型化为文件,而所有的输入输出都被当做对应文件的读和写来执行。
输入/输出(I/O)是在主存外部设备(例如磁盘驱动器,终端和网络)之间复制的过程。
输入操作: 从I/O设备复制到主存。
输出操作: 从主存复制数据到I/O设备。
打开和关闭文件
进程是通过调用open函数来打开已经存在的文件或者创建一个新的文件的:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int open(char *filename, int flags, mode_t mode);
返回:若成功则为新文件描述符,若出错为-1.
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。
*filename:文件名
flags:打开方式,设置用户权限,指明了进程打算如何访问这个文件:
- O_RDONLY:只读。
- O_WRONLY:只写。
- O_RDWR:可读可写。
flags参数也可以是一个或者更多位掩码的或(|): - O_CREAT:如果文件不存在,就创建它的一个截断的(空)文件。
- O_TRUNC:如果文件已经存在,就截断它。
- O_APPEND:在每次写操作前,设置文件位置到文件的尾处。
mode:指定了新文件的访问权限位。(若打开的文件已经存在,设置为0)
最后,进程通过调用close函数关闭一个打开的文件。
**例题1:**下面程序的输出是什么?
#include "csapp.h"
int main()
{
int fd1, fd2;
fd1 = Open("foo.txt", O_RDONLY, 0) ;
fd2 = Open("bar.txt", O_RDONLY, 0) ;
Close(fd2) ;
fd2 = Open("baz.txt", O_RDONLY, 0) ;
printf("fd2 = %d\n", fd2) ;
exit(0) ;
}
运行结果如下:
fd2 = 4
分析:
每打开一个文件,生成一个最小的正整数下标,即描述符;Linux会为每个进程自动打开三个文件:STDIN_FILENO(0),STDOUT_FILENO(1),STDERR_FILENO(2)。
所以fd1为打开的第四个文件,即描述符为3,fd2描述符为4,调用Close(fd2)关闭的文件fd2,所以文件描述符4也被关闭;随后又创建了一个新的文件fd2,描述符为4 。
读和写文件
应用程序是通过分别调用read和write函数来执行输入和输出的。
#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 。
read函数:从描述符为 fd 的当前文件位置复制最多 n 个字节到内存位置 buf 。
write函数:从内存位置 buf 复制至多n个字节到描述符 fd 的当前文件的位置 。
读取文件元数据
应用程序通过调用 stat 和 fstat 函数,检索关于文件的信息 (有时也称为文件的元数据)。
#include <unistd.h>
#include <sys/stat.h>
int stat (const char *filename , struct stat *buf) ;
int fstat (int fd , struct stat *buf) ;
返回:若成功为0,若出错为-1 。
示例代码:
查询和处理一个文件的st_mode位
#include "csapp.h"
int main(int argc, char **argv)
{
struct stat stat ;//用于存放文件信息的结构体
char *type, *readok ;
Stat(argv[1] , &stat) ;//调用stat函数,将文件argv[1]中的信息以stat结构体的形式存放到stat中
if(S_ISREG(stat.st_mode))//是否为一个普通文件
type = "regular";
else if (S_ISDIR(stat.st_mode))//是否为一个目录文件
type = "directory";
else
type = "other";
if((stat.st_mode & S_IRUSR))//用户是否可读
readok = "yes";
else
readok = "no";
printf("type: %s, read: %s\n" , type , readok) ;
exit(0) ;
}
共享文件
内核用三个相关的数据结构来表示打开的文件:
- 描述符表:每个进程都有他独立的描述符表,它的表项是由进程打开的文件描述符来索引的。
- 文件表:打开的文件的集合是由一张文件表来表示的,所有的进程共享这张表。
- v-node 表:同文件表一样,所有的进程共享这张v-node 表。
下图,展示了一个示例,其中描述符1和4通过不同的打开文件表表项来引用两个不同的文件。这是一个典型的情况,没有共享文件,并且每个描述符对应一个不同的文件。
图一:
此外,多个描述符也可以通过不同的文件表表项来引用同一个文件。
例如: 下图,如果用一个filename调用open两次,就会发生这种情况。每个描述符都有它自己的文件位置,但是两个打开的文件件表表项共享同一个磁盘文件。
图二:
假设 :在调用fork之前,父进程有如 图一 所示的打开文件。然后 图三 展示了调用fork后的情况。
由于 打开文件表 和 v-node表 都是所有进程共享 ,因此,子进程 继承了 父进程的打开文件,即父子进程共享相同的打开文件表集合 。
例题2:假设磁盘文件foobar.txt由6个ASCII码字符“foobar”组成,下列程序输出是什么?
#include "csapp.h"
int main ()
{
int fd1, fd2 ;
char c ;
fd1 = Open("foobar.txt", O_RDONLY, 0) ;
fd2 = Open("foobar.txt", O_RDONLY, 0) ;
Read(fd1, &c, 1);
Read(fd2, &c, 1);
printf("c = %c\n", c) ;
exit(0);
}
运行结构:
c = f
分析:
描述符fd1和fd2都有各自的打开文件表表项,所以每个描述符对于foobar.txt都有它自己的文件位置。因此,从fd2的读操作会读取foobar.txt的第一个字节,输出 c = f 。
例题3:
#include "csapp.h"
int main()
{
int fd ;
char c ;
fd = Open("foobar.txt", O_RDONLY, 0) ;
if (Fork() == 0) {
Read(fd, &c, 1) ;
exit(0) ;
}
Wait(NULL);
Read(fd, &c, 1) ;
printf("c = %c\n", c) ;
exit(0) ;
}
运行结果:
c = o
该题类似于 图三 ;回想一下,子进程会继承父进程的描述表,以及所有进程共享的同一个打开文件表。因此,描述符fd在父进程中都指向同一个打开文件表表项。当子进程读取文件的第一个字节时,文件位加1。因此,父进程会读取第二个字节,而输出的就是 c = o 。
I/O重定向
什么叫I/O重定向?
即 重新定向 输出 的位置,重新定向 输入 的来源。
一般使用dup2函数来进行I/O重定向的工作。
#include <unistd.h>
int dup2(int oldfd, int newfd) ;
//返回:若成功则为非负的描述符,若出错则为-1.
dup2函数复制描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。如果newfd已经打开了,dup2会复制oldfd之前关闭newfd。
如下图四,是在图一的基础之上,调用了dup2(4,1)函数的状态图结果:
两个文件描述符现在都指向文件B;文件A已经被关闭了,并且它的文件表和v-node表表项也已经被删除了;文件B的引用次数已经增加了。从此以后任何写到标准输出的数据都被重定向到文件B。
例题4:
#include "csapp.h"
int main()
{
int fd1, fd2 ;
char c ;
fd1 = Open("foobar.txt", O_RDONLY, 0) ;
fd2 = Open("foobar.txt", O_RDONLY, 0);
Read(fd2, &c, 1) ;
Dup2(fd2, fd1) ;
Read(fd1, &c, 1) ;
printf("c = %c\n", c) ;
exit(0) ;
}
运行结果:
c = o
分析:Dup2(fd2, fd1) 将文件描述符fd1 重定向到了 fd2 ,所以 fd1与 fd2都指向同一个打开文件B,由于调用了Read(fd2, &c, 1) ,所以将光标位移动到了第二位,因此再次调用时,读取的是文件B的第二个字节o。