前段时间学完csapp的系统级i/o,在这里做一点笔记
一、UNIX I/O
在UNIX系统中有一个说法,一切皆文件。所有的I/O设备,如网络、磁盘都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许UNIX内核引出一个简单、低级的应用接口,称为UNIX I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。
即
1.打开文件
内核返回一个小的非负整数,叫做描述符
等于内核分配一个文件名,来标示当前的文件。
内核记录有关这个打开文件的所有信息。应用程序只需要记住标示符。
Unix外壳创建进程时都有三个打开的文件
标准输入(标示符0)
标准输出(标示符1)
标准错误(标示符2)
头文件<unistd.h>定义了常量代替显式的描述符值
STDIN_FILENO
STDOUT_FILENO
STDERR_FILENO
2.改变当前的文件位置
文件位置是从文件开头起始的字节偏移量
3.读写文件
4.关闭文件
无论进程以何种原因终止,内核都会关闭所有打开的文件并释放它们的存储器资源。
二、打开和关闭文件
1.进程是通过调用 open函数来打开一个已存在的文件或者创建一个新文件的
int open(char *filename,int flags,mode_t mode);
//返回:若成功则为新文件描述符,若出错为-1
open函数将filename转换为一个文件描述符,并且返回描述符数字。
- 返回的描述符总是在进程当前没有打开的最小描述符。
- flags参数指明了进程打算如何访问这个文件:
- 可是以一个多个掩码的或。
O_RDONLY: 只读
O_WRONLY: 只写
O_CREAT : 如果文件不存在,就创建一个截断的(truncated)(空)文件。
O_TRUNC : 如果文件已存在,就截断它(长度被截为0,属性不变)
O_APPEND: 在每次写操作前,设置文件位置到文件的结尾
O_RDWR: 可读可写
- mode参数指定了新文件的访问权限位。
- close函数关闭一个打开的文件
int close(int fd);
//返回: 若成功则为0,若出错则为-1
三、读和写文件
通过调用read和write函数来完成输入和输出
#include <unistd.h>
ssize_t read(int fd,void *buf,size_t n);
//read函数从描述符fd的当前文件位置拷贝最多n个字节到存储器buf
返回:若成功则为读的字节数,若EOF则为0,若出错为 -1.
ssize_t write(int fd,const void *buf,size_t n)
//write函数从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置
返回:若成功则为写的字节数,若出错则为-1
四、读取文件元数据
应用程序能够通过调用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
区别是stat函数以文件名作为输入,而fstat以文件描述符作为输入.
挂一张stat的数据结构
五、共享文件
内核用相关的数据结构来表示打开的文件:
描述符表: 每个进程都有它独立述符表,每个打开的描述符表项指向文件表中的一个表项。
文件表: 打开文件的集合由一张文件表来表示,所有的进程共享这张表。每个文件表的表项包括当前的文件位置,引用计数(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个文件描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为0.
v-node表: 所有进程共享v-node表。每个表项包含相应打开文件的属性信息,包括stat结构的的大部分信息,诸如st_mode和st_size成员.
下图是典型的打开文件的内核数据结构,描述符1和4通过不同的打开文件表表项来引用两个不同的文件。没有共享文件。
图一
多个描述符也可以通过不同的文件表表项来引用同一个文件(共享),如下图所示。例如,如果以同一个filename调用open函数两次,就会发生这种情况。关键地方是每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据.
图二
对于fork的情况: 假设fork前,父进程有图一所示的打开文件。图三展示了调用fork之后的情况。子进程有一个父进程描述符的副本。父子进程共享相同的打开文件表几个,因此共享相同的文件位置。因而,在内核删除相应文件表表项之前,父子进程必须都关闭了它们的描述符.
图三
六、I/O重定向
Unix I/O提供dup2函数支持重定向
int dup2(int oldfd, int newfd);
返回: 若成功则为非负的描述符,出差则为-1
dup2拷贝描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。若newfd已经打开,dup2会在拷贝oldfd之前关闭newfd.
调用dup2(4,1)之前,初始状态如图一所示,调用之后如下图四所示.
接下来看几个相关题目
例题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;
}
输出结果:c1=a,c2=a,c3=b
解析:在此题目中,先调用open函数打开同一个文件3次,此时f1、f2、f3的值应该分别为3、4、5,然后执行 dup2(fd2, fd3)重定向指令后,f3覆盖f2,此时对fd3的操作实际是对fd2的操作,每次打开文件时,光标都位于文件的头部,当第一次调用read时直接读出该文件的第一个字母 a ,第二次 调用read时,读出的字母也为’a’,当第三次调用read时,实际上是对fd2进行操作,此时光标处于’a’位置,即他读取的是’a’的下一个字母,因此读取字母为’b’,故输出为
c1=a,c2=a,c3=b
例题2
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()) {
/* Parent */
sleep(s);
read(fd1, &c2, 1);
printf("Parent: c1 = %c, c2 = %c\n", c1, c2);
} else {
/* Child */
sleep(1-s);
read(fd1, &c2, 1);
printf("Child: c1 = %c, c2 = %c\n", c1, c2);
}
return 0;
}
输出结果:
Parent:c1=a,c2=b
Child: c1=a,c2=c
解析:首先调用getpid获取该进程的PID号,然后将与)0X01进行与运算,如果该进程pid的最后一位是0,则s=0,子进程将进入睡眠状态;若为1,则s=1,父进程将进入睡眠状态,由运行结果知子进程进入了睡眠,一开始调用read读取了abcde.txt文件中的第一个字母’a’,然后fork函数创建子进程,子进程也读取了字母’a’,然后子进程进入了睡眠状态,父进程继续读取文件的下一个字母,即’b’,因此父进程输出的是c1=a,c2=b,当子进程休眠过后,继续读取下一个字母,此时打开的仍是同一个文件,光标位于字母’b’后,即读取’b’的下一个字母’c’,故子进程输出c1=a,c2=c。
例题3
该文件最终内容是什么?
#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;
}
解析:该题中首先调用open函数创建了一个新的文件,接下来调用write函数向文件中写入四个字节’pqrs‘;
然后根据O_APPEND|O_WRONLY可知接下来调用的write是在文件的结尾加入5个字节’jklmn‘,由于都是对同一文件操作此时文件内容为’pqrsjklmn‘;
继续执行了 fd2 = dup(fd1)指令,又调用write函数,此时光标落在文件第四个字母’r‘后面,接下来对文件进行4个字节的覆盖,先对‘jklm’进行覆盖,然后在’n’前加上’wxyz’四个字节,此时文件中的内容为’pqrswxyzn’;
最后调用write在文件的结尾加上’ef’两个字节,故最后文件的内容为’pqrswxyznef’。
如有不正确之处,望各位批评指正,感谢!