Young87

SmartCat's Blog

So happy to code my life!

游戏开发交流QQ群号60398951

当前位置:首页 >跨站数据测试

CSAPP第10章 系统级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.打开文件

#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在终端上。

这样做虽然能够实现想要的功能,但是开销太大。因为每读一个字符都要调用系统级函数readwrite,系统级调用的开销比较大,因为将操作的过程全抛给操作系统 执行上下文切换 等等工作。
这个过程通常要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】没有上司的晚会

下一篇: centos7安装 mysql-8.0.16

精华推荐