10 系统级I/O
10.1 unix I/O
1.一个linux文件是一个多个字节的序列。
2.所有的I/O设备都模型化为文件
3.所有的输入输出都变成文件的读写执行
- 打开文件:内核打开文件返回一个小的非负整数作为文件描述符,后续所有的对此文件的操作都以这个描述符为标识。
- linux shell创建进程时都有三个打开的文件
标准输入(标示符0)
标准输出(标示符1)
标准错误(标示符2) - 改变文件位置:对每个打开的文件保持一个文件位置k,初始为0。文件位置即是从文件开头起始的字节偏移量
- 读写文件:一个读操作就是从文件拷贝n>0个字节到存储器,然后将k增加到k+n。
写操作就是从存储器拷贝n个字节到一个文件,从当前文件位置k开始,然后更新k。 - 关闭文件:内核释放文件打开时创建的数据结构。将描述符恢复到可用的描述符池中。
10.2 文件
- 普通文件
- 目录
- 套接字
10.3 打开和关闭文件
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int open(char *filename,int flags,mode_t mode);
//返回:若成功则为新文件描述符,若出错为-1
- filename就是文件名
- flags:指明进程如何访问这文件。
- O_RDONLY: 只读
- O_WRONLY: 只写
- O_CREAT : 如果文件不存在,就创建一个截断的(truncated)(空)文件。
- O_TRUNC : 如果文件已存在,就截断它(长度被截为0,属性不变)
- O_APPEND: 在每次写操作前,设置文件位置到文件的结尾
- O_RDWR: 可读可写
- mode指定新文件的访问权限位
进程调用close关闭一个打开的文件
#include <unistd.h>
int close(int fd);
//返回: 若成功则为0,若出错则为-1
10.4 读写文件
#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:read函数从描述符fd的当前文件位置拷贝最多n个字节到存储器buf
- write:write函数从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置
10.8 共享文件
内核有三个相关的数据结构来表示打开的文件:
-
描述符表:
每个进程都有它独立的描述符表。
它的表项是由进程打开的文件描述符来索引的。
每个打开的描述符表项指向文件表的一个表项。 -
文件表:打开文件的集合是由一张文件表表示的。
所有的进程共享这张表。
每个文件表项的部分组成是
1.当前的文件位置
2.引用计数
3.以及一个指向v-node表中对应表项的指针。 -
v-node表:
所有的进程共享这张表。每个表项包含stat结构的大多数信息
共享情形1:
同一个filename调用open函数多次,每个描述符有其自己的文件位置,对不同描述符的读操作可以从不同位置获取数据。
#include "csapp.h"
int main(int argc, char *argv[])//argv[1]为abcde.txt(abcde)
{
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);
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;
}
结果:
都是从文件的起始位置开始读。
共享情形2:
调用fork后,子进程有一个父进程描述符表副本。父子进程共享相同的打开文件表。共享相同的文件位置。
#include "csapp.h"
int main(int argc, char *argv[])
{
int fd1;
char c1, c2;
char *fname = argv[1];
fd1 = open(fname, O_RDONLY, 0);
read(fd1, &c1, 1);
if (fork() == 0) {
read(fd1, &c2, 1);
printf("Parent: c1 = %c, c2 = %c\n", c1, c2);
}
wait(NULL);
read(fd1, &c2, 1);
printf("Child: c1 = %c, c2 = %c\n", c1, c2);
exit(0);
return 0;
}
结果:
父子进程读写同一处文件位置,各自的读都相继进行。
10.9 I/O重定向
I/O重定向如何工作?
使用dup2函数
#include<unistd.h>
int dup2(int oldfd,int newfd);
返回:若成功则为非负的描述符,若出错则为-1
dup2函数拷贝描述符表表项 oldfd 到描述符表表项 newfd ,覆盖newfd。
如果newfd已经打开,dup2会在拷贝oldfd之前关闭newfd。简单的说就是把newfd的文件位置指向newfd
#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);
read(fd1, &c1, 1);
dup2(fd2, fd3);
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;
}
fd2与fd3经过dup2()之后指向同一个文件位置,读也是相继的读。
实例:
#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;
}
//结果:pqrswxyznef
fd1因为O_TRUNC,截断之前所有的内容。
之后写入pqrs。此时fd1的光标在s后面。
然后fd3因为O_APPEND,在s后面写入jklmn,此时光标在n后面。
然后fd2=dup(fd1);fd2的文件位置指向fd1。
再从s后面的光标写入wxyz,结果就是把jklm给覆盖了。
最后在fd3之后写入ef。
最后结果就是:pqrswxyznef