CSAPP第10章 系统级I/O 学习笔记
日期: 2020-12-12 分类: 跨站数据测试 490次阅读
这节内容学起来比前面轻松很多,这里笔记尽量少讲一些观念的东西,尽量搞源码。
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.打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename,int flags,mode_t mode);
返回
:若返回成功则为新文件描述符<这个是从零开始的,每打开一个文件就递增这个文件描述符,Linux shell每个进程开始时都会打开stdin(0),stdout(1),stderr(2)
flags
(指明进程如何访问这个文件):O_RNONLY:只读
O_WRONLY:只写
O_RDWR:可读可写
O_CREAT:如果文件不存在,就创建它的一个截断的(truncated)空文件。
O_TRYBC:如果文件已经存在,就截断它。
O_APPEND:在每次写操作前,设置文件位置到文件的结尾处。
mode
(制定了新文件的访问权限位)<书P625>
2.关闭文件
#include <unistd.h>
int close(int fd);
若成功返回0,失败则返回-1.
读和写文件
#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
读写时要注意的是需要判断是否读完了。看例子:
/* $begin cpstdin */
#include "csapp.c"
int main(void)
{
char c;
while(Read(STDIN_FILENO, &c, 1) != 0)
Write(STDOUT_FILENO, &c, 1);
exit(0);
}
/* $end cpstdin */
这是从终端中一个个字符地读入,直到读到EOF为止。
结果就是
为了追踪程序中函数的调用,这边用一个强大的strace
来跟踪,在终端中输入strace ./cpstdin
即可。
由于这样是会输出所有的函数过程,看着很乱,可以用strace -e trace=read,write ./cpstdin
来追踪read和write函数。
这样可以清晰的看到,读取abc和’\n’,write在终端上。
这样做虽然能够实现想要的功能,但是开销太大。因为每读一个字符都要调用系统级函数
read
和write
,系统级调用的开销比较大,因为将操作的过程全抛给操作系统 执行上下文切换 等等工作。
这个过程通常要20000到40000
个时钟周期,可以说是开销巨大。
因而后面尽量一整块数据一起读来改进效率。
在网络通信中,如果网络延迟较大或者数据太大,经常不能一次就成功读写
完所有的数据,因而确保可靠,必须用while循环来保证完全读写。下面我
们将一下健壮的I/O包。
用RIO包健壮地读写
无缓冲的输入输出指的是直接在内存和文件之间传送数据,有缓冲的输入函数值的是输入的数据先保存在读缓冲区中,再从读缓冲区读入内存。直接在csapp.c文件中看源码吧。
1.RIO的无缓冲的输入输出函数
#include "csapp.h"
ssize_t rio_readn(int fd,void *usrbuf,size_t n);
ssize_t rio_writen(int fd,void *usrbuf,size_t n);
// 参数以及返回值的意义和read,write的一样
下面来看rio_readn的源码吧
// rio_readn - robustly read n bytes (unbuffered)
/* $begin rio_readn */
ssize_t rio_readn(int fd, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nread;
char *bufp = usrbuf;
while (nleft > 0) {
if ((nread = read(fd, bufp, nleft)) < 0) {
if (errno == EINTR) /* interrupted by sig handler return */
nread = 0; /* and call read() again */
else
return -1; /* errno set by read() */
}
else if (nread == 0)
break; /* EOF */
nleft -= nread;
bufp += nread;
}
return (n - nleft); /* return >= 0 */
}
/* $end rio_readn */
如上代码所示,无缓冲的输入函数rio_readn
每次都会要求系统读取剩余的所有数据,但是可能没有成功读取所有嘛,所有用一个while循环,表示只有要求的字节数都读了或者遇到EOF才停止。
下面来看rio_writen的源码吧!
/*
* rio_writen - robustly write n bytes (unbuffered)
*/
/* $begin rio_writen */
ssize_t rio_writen(int fd, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nwritten;
char *bufp = usrbuf;
while (nleft > 0) {
if ((nwritten = write(fd, bufp, nleft)) <= 0) {
if (errno == EINTR) /* interrupted by sig handler return */
nwritten = 0; /* and call write() again */
else
return -1; /* errorno set by write() */
}
nleft -= nwritten;
bufp += nwritten;
}
return n;
}
/* $end rio_writen */
这个和上面的readn原理差不多,不多说。
2.RIO的带缓冲的输入函数
/* $begin cpfile */
#include "csapp.c"
int main(int argc, char **argv)
{
int n;
rio_t rio;
char buf[MAXLINE];
Rio_readinitb(&rio, STDIN_FILENO);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
Rio_writen(STDOUT_FILENO, buf, n);
/* $end cpfile */
exit(0);
/* $begin cpfile */
}
/* $end cpfile */
2.1 struct rio_t
/* Persistent state for the robust I/O (Rio) package */
/* $begin rio_t */
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd; /* descriptor for this internal buf */
int rio_cnt; /* unread bytes in internal buf */
char *rio_bufptr; /* next unread byte in internal buf */
char rio_buf[RIO_BUFSIZE]; /* internal buffer */
} rio_t;
/* $end rio_t */
2.2 readinitb
/*
* rio_readinitb - Associate a descriptor with a read buffer and reset buffer
*/
/* $begin rio_readinitb */
void rio_readinitb(rio_t *rp, int fd)
{
rp->rio_fd = fd;
rp->rio_cnt = 0;
rp->rio_bufptr = rp->rio_buf;
}
/* $end rio_readinitb */
2.3 rio_read
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{
int cnt;
while (rp->rio_cnt <= 0) { /* refill if buf is empty */
rp->rio_cnt = read(rp->rio_fd, rp->rio_buf,
sizeof(rp->rio_buf));
if (rp->rio_cnt < 0) {
if (errno != EINTR) /* interrupted by sig handler return */
return -1;
}
else if (rp->rio_cnt == 0) /* EOF */
return 0;
else
rp->rio_bufptr = rp->rio_buf; /* reset buffer ptr */
}
/* Copy min(n, rp->rio_cnt) bytes from internal buf to user buf */
cnt = n;
if (rp->rio_cnt < n)
cnt = rp->rio_cnt;
memcpy(usrbuf, rp->rio_bufptr, cnt);
rp->rio_bufptr += cnt;
rp->rio_cnt -= cnt;
return cnt;
}
/* $end rio_read */
2.4 rio_readlineb
/*
* rio_readlineb - robustly read a text line (buffered)
*/
/* $begin rio_readlineb */
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
{
int n, rc;
char c, *bufp = usrbuf;
for (n = 1; n < maxlen; n++) {
if ((rc = rio_read(rp, &c, 1)) == 1) {
*bufp++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
if (n == 1)
return 0; /* EOF, no data read */
else
break; /* EOF, some data was read */
} else
return -1; /* error */
}
*bufp = 0;
return n;
}
/* $end rio_readlineb */
2.5 rio_readnb
/*
* rio_readnb - Robustly read n bytes (buffered)
*/
/* $begin rio_readnb */
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nread;
char *bufp = usrbuf;
while (nleft > 0) {
if ((nread = rio_read(rp, bufp, nleft)) < 0) {
if (errno == EINTR) /* interrupted by sig handler return */
nread = 0; /* call read() again */
else
return -1; /* errno set by read() */
}
else if (nread == 0)
break; /* EOF */
nleft -= nread;
bufp += nread;
}
return (n - nleft); /* return >= 0 */
}
/* $end rio_readnb */
在有了上述的函数之后,就可以运行我们的main函数啦!
/* $begin cpfile */
#include "csapp.c"
int main(int argc, char **argv)
{
int n;
rio_t rio;
char buf[MAXLINE];
Rio_readinitb(&rio, STDIN_FILENO);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
Rio_writen(STDOUT_FILENO, buf, n);
/* $end cpfile */
exit(0);
/* $begin cpfile */
}
/* $end cpfile */
跟踪一下运行:
可以看到这种缓冲区的优势就是读写的次数很少。
共享文件
每个进程都有一个独立的`描述符表`,每个索引都是指向打开文件表的一个表项目。
所有进程共享一个打开文件表,包括了该文件的位置,引用计数以及一个指向v-node的指针
所有进程共享v-node表,这里包含了stat结构的大多数信息,如文件类型,访问权限,文件大小...
test1
:同个文件可以多开几个,但是开几个就只能关几个,多关就报错。这暗示着open的实质就是在当前进程中创建一个指向某文件的描述符(猜测),close就是删除该项描述符。每个进程关闭时这个表就没了。
#include "csapp.c"
int main()
{
// 说明同个文件可以多开几个,但是开几个就只能关几个,多关就报错
int id1=Open("b.c",O_RDONLY,S_IROTH);
int id2=Open("b.c",O_RDONLY,S_IROTH);
printf("%d,%d\n",id1,id2);
Close(id1);
Close(id2);
Close(id2);
return 0;
}
test2
#include "csapp.c"
int main()
{
int id_child=0;
int id_father=0;
int fk=Fork();
if (fk==0)
{
id_child=Open("abcdef.c",O_RDONLY,S_IROTH);
char c;
read(id_child,&c,1);
write(1,&c,1);
}
else
{
id_father=Open("abcdef.c",O_RDONLY,S_IROTH);
char c;
read(id_father,&c,1);
write(1,&c,1);
}
printf("\n id_child=%d,id_father=%d\n",id_child,id_father);
exit(0);
}
这里是显然的,这是在创建子进程后才各自打开文件的,每个进程独立有自己的描述符表,他们打开的文件不是同一个,因而文件位置不会相互影响。再来试试创建子进程前就打开吧。
test3
#include "csapp.c"
int main()
{
int id=Open("abcde.c",O_RDONLY,S_IROTH);
int fk=Fork();
if (fk==0)
{
char c;
read(id,&c,1);
write(1,&c,1);
}
else
{
char c;
read(id,&c,1);
write(1,&c,1);
}
close(id);
exit(0);
}
符合预期,他们打开的是同一个文件,所以会更改文件位置而相互影响,两个进程都有这个文件描述符,因而都需要关闭,所以close两次。
经过上面几个
test
可以知道,每次open都会为当前进程创建一个文件描述符,并且在打开文件表中创建一个表,这个很关键,打开文件表是描述符表都是在open的时候创建的。
v-node
是每个文件都有的,每个文件在v-node只有一份,任何打开文件表都指向该文件的文件符号表。(我感觉在功能上来说,完全可以吧v-node表整合到打开文件表中,完全没问题。只是它们分开的话会占用最少的内核内存)
IO重定向
linux shell提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。例如键入
ls > foo.txt
使得shell加载和执行ls程序时,将标准输出重定向到磁盘文件foo.txt,dup2
函数就是其中一种重定位函数。
#include <unistd.h>
int dup2(int oldfd,int newfd)
dup2函数复制描述符表表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。
ffiles1
#include "csapp.c"
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);// 用fd3指向的文件为fd2指向的文件
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;
}
显然的,不用多说。
ffiles2
#include "csapp.c"
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;
}
这个结果也是符合预期。
ffiles3
#include "csapp.c"
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,依然是符合预期。这里的启示是,可以对同个文件同时打开多个,是可以并发地写或读数据的。
感觉重定向是个很有用的功能。
除特别声明,本站所有文章均为原创,如需转载请以超级链接形式注明出处:SmartCat's Blog
上一篇: 【SSL_1607】没有上司的晚会
精华推荐