20145214 《信息安全系统设计基础》第9周学习总结
教材学习内容总结
Unix I/O
-
输入/输出是在主存和外部设备(如磁盘驱动器、终端和网络)之间拷贝数据的过程。输入操作时从I/O设备拷贝数据到主存,而输出操作时从主存拷贝数据到I/O设备
-
所有的I/O设备,如网络、磁盘盒终端,都被模型化为文件,而所有的输入和输出都被当做对相应的文件的读和写来执行。这是一种应用接口,称为Unix I/O
打开文件:Unix外壳创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(为1)、标准错误(为2) 改变当前的文件位置:应用程序能够通过执行seek操作,显式地设置文件的当前位置为k 读写文件:一个读操作就是从文件拷贝n>0个字节到存储器,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时执行读操作会触发一个称为end-of -file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF”符号。类似的,写操作就是从存储器拷贝n>0个字节到一个文件,从当前文件位置k开始,然后更新k 关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的存储器资源
打开和关闭文件
-
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件
O_RDONLY:只读。 O_WRONLY:只写。 O_RDWR:可读可写。
-
flags参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:
O_CREAT:如果文件不存在,就创建它的一个截断的(空的)文件。 O_TRUNC:如果文件已经存在,就截断它。 O_APPEND:在每次写操作前,设置文件位置到文件的结尾处。
-
mode参数指定了新文件的访问权限位。这些位的符号名字如下图所示,在sys/stat.h中的定义
-
作为上下文的一部分,每个进程都是一个umask,它是通过调用umask函数来设置的。当进程通过带某个mode参数的open函数调用来创建一个新文件时,文件的访问权限位被设置为
mode & ~umask
读和写文件
-
read函数从描述符为fd的当前文件位置拷贝最多n个字节到存储器位置buf,返回值-1表示一个错误。而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。
-
write函数从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置。
-
展示了一个程序使用read和write调用一次一个字节地从标准输入拷贝到标准输出代码如下
-
在某些情况下,read和write传送的字节比应用程序要求的要少原因如下:
1、读时遇到EOF。假设我们猪呢比读一个文件,该文件从当前文件位置开始只含有20多个字节,而我们以50个字节的片进行读取。这样一来,下一个read返回的不足值为20,此后的read将通过返回不足值0来发出EOF信号。 2、从终端读文本行。如果打开文件是与终端相关联的(如键盘和显示器),那么每个read函数将以此传送一个文本行,返回的不足值等于文本行的大小。 3、读和写网络套接字。如果打开的文件对应于网络套接字,那么内部缓冲约束和较长的网络延迟会引起read和write返回不足值。对Unix管道调用read和write时,也有可能出现不足值,这种进程间的通信机制不在我们讨论的范围之内。
用RIO包健壮地读写
-
RIO包会自动处理不足值。RIO提供了两类不同的函数:
无缓冲的输入输出函数。这些函数直接在存储器和文件之间传送数据,没有应用级缓冲,他们对将二进制数据读写到网络和从网络读写二进制数据尤其有用。 带缓冲的输入函数。这些函数允许你高效地从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内,类似于像printf这样的标准I/O函数提供的缓冲区。是线程安全的,它在同一个描述符上可以被交错地调用
-
通过调用
rio_readn
和rio_writen
函数,应用程序可以在存储器和文件之间直接传送数据 -
rio_readn
函数从描述符fd的当前文件位置最多传送n和字节到存储器位置usrbuf。类似地,rio_writen
函数从位置usrbuf传送n个字节到描述符fd。rio_readn
函数在遇到EOF时只能返回一个不足值。rio_writen
函数绝不会返回不足值。对于同一个描述符,可以任意交错地调用rio_readn
和rio_writen
-
用一个程序来计算文本文件中文本行的数量:用read函数来一次一个字节地从文件传送到用户存储器,检查每个字节来查找换行符。这个方法的缺点是效率低,每读取文件中的一个字节都要求陷入内核
-
另一种方法是调用一个包装函数(
rio_readlineb
),它从一个内部读缓冲区拷贝一个文本行,当缓冲区变空时,会自动地调用read
重新填满缓冲区。对于既包含文本行也包含二进制数据的文件,我们也提供了一个rio_readn
带缓冲区的版本,叫做rio_readnb
,它从和rio_readlineb
一样的读缓冲区中传送原始字节 -
从标准输入拷贝一个文本文件到标准输出的代码示例如下
int main(int argc, char **argv) { int n; rio_t rio; char buf[MAXLINE]; Rio_readinitb(&rio, STDIN_FILENO);//连接标准输入和rio地址 while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) //当成功返回时,将rio中的内容拷贝到存储器位置buf中,最多读maxline-1 Rio_writen(STDOUT_FILENO, buf, n);//把存储器位置中的数据拷贝到标注输出中。 exit(0); }
-
先连接标准输入和地址rio,再根据返回值判断是否成功将rio中的一行内容拷贝到了buf中,如果是再把这一行拷贝到标准输出中,即可实现一次一行的从标准输入拷贝一个文本文件到标准输出。
读取文件元数据
-
元数据即文件信息,需要用到的函数是stat和fstat
-
stat需要输入文件名,而fstat需要输入的是文件描述符。
-
文件类型:
普通文件:二进制或文本数据,宏指令:S_ISREG() 目录文件:包含其他文件的信息,宏指令:S_ISDIR() 套接字:通过网络和其他进程通信的文件,宏指令:S_ISSOCK()
-
Unix提供的宏指令根据st_mode成员来确定文件的类型
-
查询和处理一个文件的st_mode位示例如下:
共享文件
-
内核用三个相关的数据结构来表示打开的文件:
描述符表:每个打开的描述符表项指向文件表中的一个表项 文件表:所有进程共享这张表,每个表项包括文件位置,引用计数,以及一个指向v-node表对应表项的指针 v-node表:所有进程共享这张表,包含stat结构中的大多数信息
-
三种打开文件的类型:
典型:描述符各自引用不同的文件,没有共享 共享:多个描述符通过不同的文件表表项引用同一个文件。(关键思想:每个描述符都有自己的文件位置,对不同描述符的读操作可以从文件的不同位置获取数据) 继承:子进程继承父进程打开文件。调用fork后,子进程有一个父进程描述符表的副本,父子进程共享相同的打开文件表集合,因此共享相同的文件位置
-
典型的无共享文件打开如图,描述符各自引用不同的文件,没有共享
-
文件共享:多个描述符通过不同的文件表表项引用同一个文件。如下图。(关键思想:每个描述符都有自己的文件位置,对不同描述符的读操作可以从文件的不同位置获取数据)
-
继承:子进程继承父进程打开文件。调用fork后,子进程有一个父进程描述符表的副本,父子进程共享相同的打开文件表集合,因此共享相同的文件位置。如下图。在内核删除相应文件表表项之前,父子进程必须都关闭了它们的描述符。
I/O重定向
-
Unix外壳提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来
-
重定向使用dup2函数
int dup2(int oldfd,int newfd);
-
dup2函数拷贝描述符表表项oldfd到描述符表表项newfd,覆盖描述表表项newfd以前的内容,如果newfd已经打开,dup2会在拷贝oldfd之前关闭newfd
标准I/O
-
ANSI C定义了一组高级输入输出函数,称为标准I/O库。提供了打开和关闭文件的函数(fopen和fclose),读和写字节的函数(fread和fwrite),读和写字符串的函数(fgets和fputs),格式化I/O函数(scanf和printf)
-
标准I/O库将一个打开的文件模型化为一个流。一个流就是一个指向FILE类型的结构的指针
-
每个ANSI C程序开始时都有三个打开的流stdin、stdout、stderr,分别对应标准输入、标准输出、标准错误
-
类型为FILE的流是对文件描述符和流缓存区的抽象
-
对流I/O的限制及解决:
限制1:跟在输出函数之后的输入函数。通过采用在每个输入操作前刷新缓冲区这样的规则来满足 限制2:跟在输入函数之后的输出函数。通过对同一个打开的套接字描述符打开两个流,一个用来读,一个用来写
-
用健壮的RIO函数,如果需要格式化的输出,使用sprintf函数在存储器中格式化一个字符串,然后用rio_writen把它发送到套接口;如果需要格式化的输入,使用rio_readlineb来读一个完整的文本行,然后用scanf从文本行提取不同的字段
教材学习中遇到的问题及解决
根据代码驱动的程序设计学习建立项目结构后,在对P449面的代码调用GCC驱动程序时命令出错
-
第一次出错是因为将
-O1
误输成了-01
,改正了这个错误后又提示了swap.c没有这个文件或目录
,猜测是因为没有给swap.c指明路径,于是修正指令如下
-
修正后不再报错,于是可以得出:有多个.c文件时,需要对每一个文件指明相对路径,即使这些文件都在同一个路径下
练习题完成过程遇到的问题及解决
练习题10.1
-
编译练习题10.1代码时提示头文件出错
-
怀疑是因为没有对应的头文件,于是进入/usr/include文件夹,通过grep进行查找,无法找到相应的头文件
-
由于“csapp.h”这个是这本教材编写的一系列头文件,不是计算机自带的,发现可以从网上下载下来并且成功运行
-
open函数总是返回最低的未打开的描述符,所以第一次调用open会返回描述符3。调用close函数会释放描述符3.最后对open的调用会返回描述符3,因此程序的输出应该是
“fd2 = 3”
练习题10.2
-
在本题中,fd1和fd2有独立的文件描述符,所以是典型的没有共享的打开文件方式
-
它们各自有各自的描述符表、文件表、v-code表,每个描述符对于foobar.txt都有它自己的文件位置,所以它们的读取是各自独立的,
-
从fd2的读操作会读取foobar.txt的第一个字节,因此最后得值是f,输出
“c = f”
练习题10.3
-
本题中用的是子进程继承父进程打开文件的方式,Fork是子程序,和父程序共享同一个描述符表、文件表、v-code表,指向相同的文件
-
描述符fd在父子进程中都指向同一个打开文件表表项,子进程执行到
Read(fd, &c, 1);
时,子进程读取文件的第一个字节并且将文件位置加1 -
之后父程序在其基础上进行,读取下一个字符,是o,最后输出
“c = o”
练习题10.4
-
要使重定向标准输入(描述符0)到描述符5,我们可以调用
dup2(5,0)
-
等价于
dup2(5,STDIN_FILENO)
练习题10.5
-
题目所给的程序中与10.2、10.3最大的不同是增加了语句
Dup2(fd2,fd1);
-
初始情况下fd1和fd2的描述符分别是3和4,所以是两个不同描述符表,指向两个不同的文件,但是由于在读了fd2一个字节之后,将fd1重定向到了fd2,所以此时再读fd1相当于在读fd2,也就是输出结果为
“c = o”
代码托管情况
代码托管链接
代码托管截图
代码行数统计
其他(感悟、思考等,可选)
- 本周学习的是Unix I/O,以及各种I/O包,对文件的打开和关闭、读和写等操作又有了更深的理解,超越了之前只知道应该如何使用
fopen
,fread
,fwrite
等等标准I/O函数,本周的学习之后又知道了有rio_readn
,rio_writen
等等更健壮的RIO函数可以使用。 - 本周学习中遇到的一个较为棘手的问题是程序以及练习题代码中出现的
csapp.h
这个头文件,由于“csapp.h”这个是这本教材编写的一系列头文件,不是计算机自带的,上网查询后发现可以从网上下载下来。如果这个问题没有解决的话就无法对练习题给出的代码进行测试,只能通过书上的理论知识得出理论上程序应该有的输出结果。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第零周 | 0/0 | 1/1 | 5/5 | 使用虚拟机安装linux系统,安装ubuntu |
第一周 | 100/100 | 1/2 | 20/25 | 掌握核心的linux命令,了解了linux操作系统 |
第二周 | 76/176 | 1/3 | 30/55 | 学会了虚拟机上的C编程 |
第三周 | 214/390 | 1/4 | 20/75 | 初步学习计算机中各种数的表示和运算 |
第五周 | 138/528 | 1/5 | 25/100 | 通过学习汇编,了解逆向的思想应用 |
第六周 | 150/678 | 1/6 | 30/130 | 安装了Y86处理器,了解了ISA抽象 |
第七周 | 100/778 | 1/7 | 20/150 | 理解了局部性原理和缓存思想在存储层次结构中的应用 |
第八周 | 0/778 | 2/9 | 20/170 | 对前七周的内容进行了查缺补漏 |
第九周 | 77/855 | 2/11 | 25/195 | 学习了Unix I/O并且了解了Unix I/O的使用情况 |