2017-2018-1 《信息安全系统设计基础》 第十三周学习总结
学习要求
找出全书你认为最重要的一章,深入重新学习一下,要求(期末占10分):
- 完成这一章所有习题
- 详细总结本章要点
- 给你的结对学习搭档讲解你的总结并获取反馈
- 参考上面的学习总结模板,把学习过程通过博客(随笔)发表,博客标题“学号 《信息安全系统设计基础》第十三周学习总结”,博客(随笔)要通过作业提交,截至时间本周日 23:59。
学习总结
我选择的章节是第十章:系统级I/O
我选择这章的原因是:个人觉得目前我们使用计算机主要还是和程序及其设计语言打交道,在学Linux的时候听过有一句话叫:”一切皆文件。”,故而觉得掌握文件与文件之间的数据交换是有必要的,而建立在其之上的就是各种程序和应用,可以说有了I/O,数据构成了一个系统,能够处理更加复杂的信息。
-> Unix I/O
简而言之,所有的I/O设备都可以被模型化为文件,输入和输出就是文件的读和写。这种映射为文件的方式,由Linux内核引出了一个低级简单的接口,就是Unix I/O
-> 文件
所有的输入和输出都能以一下方式执行:
- 打开文件
- 读写文件
- 关闭文件
我们来了解一下Linux文件和它的类型:
- 普通文件,分为文本文件和二进制文件
- 目录,包含一组链接,里面记录了文件名到文件的映射
- 套接字,通信文件
- 命名通道
- 符号链接
- 字符和块设备
……
目录层次结构中的位置用路径名来指定,包含两种形式:
- 绝对路径名,从根节点开始
- 相对路径名,从当前工作目录开始
-> 文件操作
进程通过调用open函数来打开或者创建文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode);
一个应用程序通过要求内核来打开文件,内核返回一个小的非负整数(描述符),内核记录有关这个文件的所有的信息,应用程序只需要记住这个描述符。flag参数指明了如何访问这个文件:
-
O_RDONLY: 只读
-
O_WRONLY: 只写
-
O_RDWR: 可读可写
mode参数制定了新文件的访问权限位:
所有文件类型都具有访问权限。每个文件有 9 个访问权限位.
关闭文件会调用close函数:
#include <unistd.h>
int close(int fd);
在系统I/O中读写文件用的系统函数为read()和write()函数来执行。
#include <unistd.h>
ssize_t read(int fd,void * buf,size_t n);
ssize_t write(int fd,void *buf,size_t n);
read函数从描述符为fd的当前文件位置拷贝最多n个字节到存储器位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。而write函数从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置。返回值要么为-1要么为写入的字节数目。
/* $begin cpstdin */
#include "csapp.h"
int main(void)
{
char c;
while(Read(STDIN_FILENO, &c, 1) != 0)
Write(STDOUT_FILENO, &c, 1);
exit(0);
}
/* $end cpstdin */
关于在文件中定位使用的函数为lseek,在I/O库中使用的函数为fseek。
- 问题:size_t和ssize_t的区别
- 解:前者是unsigned int,而后者是int)
有些情况下,read和write传送的字节比应用程序要求的要少,出现这种情况的原因如下:
- 读时遇到EOF。此时read返回0来发出EOF信号。
- 从终端读文本行。如果打开文件是与终端相关联,那么每个read函数将以此传送一个文本行,返回的不足值等于文本行的大小。
- 读和写网络套接字。可能会出现阻塞现象。
实际上,除了EOF,在读磁盘文件时,将不会遇到不足值,而且在写磁盘文件时,也不会遇到不足值。然而,如果你想创建健壮的网络应用,就必须反复调用read和write处理不足值,直到所有需要的字节都传送完毕。(这一点在网络编程中已经体会到了)
-> RIO包
RIO提供了方便、健壮和高效的I/O。提供了两类不同的函数:
- 无缓冲的输入输出函数 直接在存储器和文件之间传送数据,没有应用级缓冲,它们对将二进制数据读写到网络和从网络读写二进制数据尤其有用。
- 带缓冲的输入函数
ssize_t rio_readn(int fd,void *usrbuf,size_t n);
ssize_t rio_writen(int fd,void *usrbuf,size_t n);
对同一个描述符,可以任意交错地调用rio_readn和rio_writen。一个问本行的末尾都有一个换行符,那么像读取一个文本中的行数怎么办,使用read读取换行符这个方法不是很妥当,可以调用一个包装函数(rio_readineb),它从一个内部读缓冲区拷贝一个文本行,当缓冲区为空时,会自动地调用read重新填满缓冲区。也就是说,这些函数都是缓冲区操作而言的。
下面这个例子是通过RIO函数一次一行的从标砖输入复制一个文本文件到标准输出:
#include "csapp.h"
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 */
}
-> 读取文件元数据
应用程序能够通过调用stat和fstat函数检索到关于文件的信息(有时也称为文件的元数据)
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *filename,struct stat *buf);
int fstat(int fd,struct stat *buf);
若成功,返回0,若出错则为-1.stat以一个文件名为输入,并且填充buf结构体。fstat函数只不过是以文件描述符而不是文件名作为输入。
struct stat {
#if defined(__ARMEB__)
unsigned short st_dev;
unsigned short __pad1;
#else
unsigned long st_dev;
#endif
unsigned long st_ino;
unsigned short st_mode;
unsigned short st_nlink;
unsigned short st_uid;
unsigned short st_gid;
#if defined(__ARMEB__)
unsigned short st_rdev;
unsigned short __pad2;
#else
unsigned long st_rdev;
#endif
unsigned long st_size;
unsigned long st_blksize;
unsigned long st_blocks;
unsigned long st_atime;
unsigned long st_atime_nsec;
unsigned long st_mtime;
unsigned long st_mtime_nsec;
unsigned long st_ctime;
unsigned long st_ctime_nsec;
unsigned long __unused4;
unsigned long __unused5;
};
其中st_size成员包含了文件的字节大小。st_mode为文件访问许可位。UNIX提供的宏指令根据st_mode成员来确定文件的类型:
- S_ISREG(),这是一个普通文件
- S_ISDIR(),这是一个目录文件
- S_ISSOCK()这是一个网络套接字
运行一下书上的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int fd,size;
struct stat buf_stat;
memset(&buf_stat,0x00,sizeof(buf_stat));
fd=stat("stat.c",&buf_stat);
printf("%d
",(int)buf_stat.st_size);
return 0;
}
运行结果为:
查询和处理一个文件的 st_mode位:
#include "csapp.h"
int main (int argc, char **argv)
{
struct stat stat;
char *type, *readok;
/* $end statcheck */
if (argc != 2) {
fprintf(stderr, "usage: %s <filename>
", argv[0]);
exit(0);
}
/* $begin statcheck */
Stat(argv[1], &stat);
if (S_ISREG(stat.st_mode)) /* Determine file type */
type = "regular";
else if (S_ISDIR(stat.st_mode))
type = "directory";
else
type = "other";
if ((stat.st_mode & S_IRUSR)) /* Check read access */
readok = "yes";
else
readok = "no";
printf("type: %s, read: %s
", type, readok);
exit(0);
}
-> 读取目录
主要介绍了是三个函数:
- readdir
- opendir
- closedir
readdir函数调用:
#include<sys/types.h>
#include<dirent.h>
struct dirent *readdir(DIR *dirp);
readdir函数调用返回的是指向流dirp中下一个目录项的指针,如果没有就是null。
这里的流是对条目有序列表的抽象(目录项列表)
opendir函数需要输入路径名为参数,并且返回指向目录流的指针
#include<dirent.h>
DIR *opnedir(const char *name);
closedir函数会关闭流并释放其所有资源
#include<dirent.h>
int closedir(DIR *dirp);
-> 共享文件
内核用三个相关的数据结构来表示打开的文件:
- 描述符表,每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
- 文件表,打开文件的描述符表项指向问价表中的一个表项。所有的进程共享这张表。每个文件表的表项组成包括由当前的文件位置、引用计数(既当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的应用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
- v-node表,同文件表一样,所有的进程共享这张v-node表,每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员。
通过下面这张图可以方便理解:
父子进程也是可以共享文件的,在调用fork()之前,父进程如上图,然后调用fork()之后,子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此共享相同的文件位置。一个很重要的结果就是,在内核删除相应文件表表项之前,父子进程必须都关闭了他们的描述符。
下面这个图是我在网上找到的,展示了文件描述符、打开的文件句柄以及i-node之间的关系,比较直观:
在进程A中,文件描述符1和30都指向了同一个打开的文件句柄(标号23)。这可能是通过调用dup()、dup2()、fcntl()或者对同一个文件多次调用了open()函数而形成的。
进程A的文件描述符2和进程B的文件描述符2都指向了同一个打开的文件句柄(标号73)。这种情形可能是在调用fork()后出现的(即,进程A、B是父子进程关系),或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程时,也会发生。再者是不同的进程独自去调用open函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
此外,进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表的相同条目(1976),换言之,指向同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了open()调用。同一个进程两次打开同一个文件,也会发生类似情况。
-> I/O重定向
其用到的函数为:
#include<unistd.h>
int dup2(int oldfd, int newfd);
书上的例子是:dup2(4,1)
假设在调用dup2(4,1)之前,我们的状态图10-11所示,其中描述符1(标准输出)对应于文件A(比如一个终端),描述符4对应于文件B(比如一个磁盘文件)。A和B的引用计数都等于1。图10-14显示了调用dup2(4,1)之后的情况。两个描述符现在都指向了文件B;文件A已经被关闭了,并且它的文件表和v-node表表项也已经被删除了;文件B的引用计数已经增加了。从此之后,任何写到标准输出的数据都被重定向到文件B。
-> 标准I/O
标准的I/O是由ANSI C标准规定的,不仅可以在UNIX/Linux上使用,还可以在其他地方使用。
标准I/O库对应文件的操作是围绕流(stream)进行的。当用标准I/O库打开或创建文件时,便使一个流与一个文件相关联。标准I/O库的函数对于文件的描述是基于FILE对象指针的,该对象是一个结构体,包含了I/O库管理流所需要的全部信息:用于实际I/O的文件描述符、指向流缓存的指针、缓存长度、当前在缓存中的字符数、出错标志等等。
和文件描述符STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO类似,标准I/O也有标准输入、标准输出、标准出错这三项,每个ANSI C开始时都有三个打开的流:
- stdin
- stdout
- stderr
它们是通过预定义的指针stdin、stdout、stderr来引用的。这三个指针定义于头文件<stdio.h>。
课本习题
我研究了一下书上的练习题:
- 10.1
#include "csapp.h"
int main()
{
int fd1, fd2;
char c;
fd1 = Open("foobar.txt", O_RDONLY, 0);
fd2 = Open("foobar.txt", O_RDONLY, 0);
Read(fd1, &c, 1);
Read(fd2, &c, 1);
printf("c = %c
", c);
exit(0);
}
运行结果:
- 遇到问题:
课后习题:
下面是解答:
-
10.6
输出 fd2 = 4
已经有0 1 2被打开,fd1是3,fd2是4,关闭fd2之后再打开,还是4。 -
10.7
运行:
int main(int argc, char **argv)
{
int n;
rio_t rio;
char buf[MAXBUF];
Rio_readinitb(&rio, STDIN_FILENO);
while((n = Rio_readnb(&rio, buf, MAXBUF)) != 0)
Rio_writen(STDOUT_FILENO, buf, n);
}
-
10.8
只需要将stat那句话改为: fstat(atoi(argv[1]), &stat);
当然,如果需要加其他处理的话(比如判断参数对错,fd是否存在等等),还需要添加一些语句。 -
10.9
这里应该是表明,输入重定向到了foo.txt,然而3这个描述符是不存在的。
说明foo.txt并没有单独的描述符3。
所以Shell执行的代码应该是这样的:
if (Fork() == 0) {/* Child */
int fd = open("foo.txt", O_RDONLY, 0);
dup2(fd, 1);
close(fd);
Execve("fstatcheck", argv, envp);
}
- 10.10
这里使用一个重定向的技术即可。如果参数个数为2,那么就将标准输入重定向到文件。
程序并没有检测各种错误。
int main(int argc, char **argv)
{
int n;
rio_t rio;
char buf[MAXLINE];
if(argc == 2){
int fd = open(argv[2], O_RDONLY, 0);
dup2(fd, STDIN_FILENO);
close(fd);
}
Rio_readinitb(&rio, STDIN_FILENO);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
Rio_writen(STDOUT_FILENO, buf, n);
}
上周考试错题分析
本周结对学习情况
- 结对学习博客
20155302 - 结对学习图片
- 结对学习内容
- 教材第八章、第十章
- 实验五
代码托管
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 0/0 | 1/1 | 10/10 | |
第三周 | 200/200 | 2/3 | 10/20 | |
第四周 | 100/300 | 1/4 | 10/30 | |
第五周 | 200/500 | 3/7 | 10/40 | |
第六周 | 500/1000 | 2/9 | 30/70 | |
第七周 | 500/1500 | 2/11 | 15/85 | |
第八周 | 223/1723 | 3/14 | 15/100 | |
第九周 | 783/2506 | 3/17 | 15/115 | |
第十周 | 0/2506 | 3/20 | 12/127 | |
第十一周 | 620/3126 | 2/22 | 20/147 | |
第十二周 | 390/3516 | 2/24 | 17/164 | |
第十三周 | 812/4328 | 2/26 | 30/194 |