I/O也就是我们常说的输入/输出,而所有的I/O设备都被模型化为文件,所有的输入输出都被当作对相应文件的读和写来执行。这种文件映射就是这一章节所要学习的部分。
描述符
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。如此一来,内核会记录有关这个打开文件的所有信息,而应用程序只需要记住这个描述符。
文件
文件主要分为普通文件/目录和套接字:
- 普通文件:包含任意数据,其中又分为只含有ASCII或Unicode字符的文本文件和二进制文件,对于内核而言,二者没有区别。
- 目录:包含一组链接,每一个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录,每个目录至少包含两个条目, . 是到该目录自身的链接, . . 是到该目录父级目录的链接。可以使用mkdir创建一个目录,可以使用ls查看这个目录下所有的文件,还可以使用rmdir删除该目录。
- 套接字:用来与另一个进程进行跨网络通信的文件。
- 其它类型:命名通道/符号链接/字符和块设备等。
目录层次结构
Linux内核将所有文件都组织成一个目录层次结构,由命名为 / 的根目录确定。系统中的每个文件都是它的直接或间接后代。
每个进程都有一个当前工作目录确定其在目录层次结构中的当前位置,可以使用 cd 修改shell中的当前工作目录(也就是前进或者后退)。
目录层次结构中的位置用路径名指定,路径名是一个字符串,包括一个可选斜杠,其后紧跟一系列的文件名,文件名之间通过斜杠分隔。路径名有两种模式:
- 绝对路径名:以一个修杠开始,表示从跟节点开始的路径。
- 相对路径名:以文件名开始,表示从当前工作目录下开始的路径。
打开文件
进程通过调用open函数打开一个已存在或创建一个新文件:
int open(char *filename, int flags, mode_t mode)
成功则返回新文件的描述符,出错则返回-1。open函数将filename 转换成一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:
- O_RDONLY:只读。
- O_WRONLY:只写。
- O_RDWR:可读可写。
flags参数也可以是一些用作提示的掩码:
-
O_CREAT:如果文件不存在,就创建一个截断的(空)文件。
-
O_TRUNC:如果文件已存在,则截断它(删除内容)。
-
O_APPEND:再没次写操作前,设置文件位置到文件的结尾处。
例:fd = Open( "foo.txt" , O_WRONLY| O_APPEND , 0); ps:open 是系统自带的函数,而Open则是实例操作中自己定义的函数,在Open函数中对于不能打开的行为做了异常处理,相当于优化了系统open函数。
mode 参数制定了新文件的访问权限位。
每一个进程都拥有两个宏定义DEF_MODE/DEF_UMASK,分别默认设置了相关参数:
#define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK S_IWGRP |S_IWOTH
关于访问权限位的参数设置如下:
S_IRUSR | 仅拥有者能读该文件 |
---|---|
S_IWUSR | 仅拥有者能写该文件 |
S_IXUSR | 仅拥有者能执行该文件 |
S_IRGRP | 拥有者所在组成员能读该文件 |
S_IWGRP | 拥有者所在组成员能写该文件 |
S_IXGRP | 拥有者所在组成员能执行该文件 |
S_IROTH | 任何人能读该文件 |
S_IWOTH | 任何人能写该文件 |
S_IXOTH | 任何人能执行该文件 |
关闭文件
int close(int fd);
读文件
ssize_t read(int fd , void *buf , size_t n);
read函数从描述符为fd的当前位置复制最多n个字节到内存位置buf.
若成功则返回所读字节数,错误则返回-1,EOF返回0
实例
#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;
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap10_code$ gcc ffiles1.c csapp.h csapp.c -lpthread -o ffiles1
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap10_code$ ./ffiles1 abcde.txt
c1 = a, c2 = a, c3 = b
其中,abcde.txt是一个只含有abcde五个字符的文本文件,Open函数和Read函数以及Close函数都是头文件中所构写的。其中dup2函数是重定向函数。
重定向
原dup2函数如下:
int dup2(int oldfd, int newfd);
dup2函数复制描述符表(后面解释)表项oldfd到描述符表表项newfd,覆盖描述符表表项newfd以前的内容。
如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd.
这样一来,刚刚上面所讲到的实例中fd3实质上就等于fd2,当打开描述符fd3所指代的文件时,实质上就是打开fd2所指向的文件,因为该文件已在fd2中打开过一次,故本次打开时会自动紧接着上一次读取操作之后进行读取,于是读取出“b”字符。
除此函数外,重定向也可以由shell命令行中操作符“<”和“>”执行,如:
linux> ls > foo.txt
在linux中直接使用>就是将左边的ls标准输出重定向到foo.txt文件。
写文件
ssize_t write(int fd , const void *buf , size_t n);
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置.
若成功则返回所写字节数,错误返回-1.
其中,ssize_t和size_t有所区别:
- ssize_t:long型.
- size_t:unsigned long型.
读取文件元数据
元数据,文件的信息描述。
应用程序能够通过stat和fstat两个函数,检索到文件的元数据:
int stat(const char *filename, strut stat *buf);
stat函数以一个文件名作为输入,并填写一个stat数据结构中的各个成员。
int fstat(int fd, struct stat *buf);
fstat是以文件描述符作为输入。
这是其结构体定义(以下部分借鉴该篇博客):
struct stat {
dev_t st_dev; /* 块设备号(ID) */
ino_t st_ino; /* inode结点号,文件属性信息所存inode节点的编号 */
mode_t st_mode; /* 文件类型和文件权限*/ ls
nlink_t st_nlink; /* 链接数 */ ls
uid_t st_uid; /* 文件所属用户ID*/ ls
gid_t st_gid; /* 文件所属组ID */ ls
dev_t st_rdev; /* 字符设备ID */
off_t st_size; /* 文件大小 */
blksize_t st_blksize; /* 系统每次按块Io操作时,块的大小(一般是512或1024) */
blkcnt_t st_blocks; /* 块的索引号 */
time_t st_atime; /* 最后一次访问时间,read*/ ls
time_t st_mtime; /* 最后一次修改时间,write */
time_t st_ctime; /* 最后一次属性修改的时间,如权限被修改,文件所有者(属主)被修改 */
};
我们可以实际来看一下上面一直提到的abcde.txt的文件信息:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include "csapp.h"
int main(int argc, char *argv[]) {
struct stat buf; //stat函数获取到的文件信息最终保存在struct stat结构体中
int fd1;
char *fname = argv[1];
fd1 = Open(fname, O_RDONLY, 0);
int ret = fstat(fd1, &buf);
//ret返回-1表示出错
if(ret == -1){
perror("stat error");
exit(1);
}
printf("st_ino = %ld\n", buf.st_ino); //inode号
printf("st_size = %ld\n", buf.st_size); //文件大小
printf("st_nlink = %ld\n", buf.st_nlink); //硬链接数
printf("st_uid = %d\n", buf.st_uid); //用户id
printf("st_gid = %d\n", buf.st_gid); //组id
printf("st_mode = %x\n", buf.st_mode); //文件属性权限
return 0;
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap10_code$ gcc fstat.c csapp.h csapp.c -lpthread -o fstat
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap10_code$ ./fstat abcde.txt
st_ino = 661818
st_size = 5
st_nlink = 1
st_uid = 1000
st_gid = 1000
st_mode = 81a4
具体不细说,此处仅提供实践参考。至于编译时所使用的-lpthread指令是因为csapp.h中具有线程相关内容,这里也不加详述。
这里有书上一个关于stat函数检查文件的代码段:
/* $begin statcheck */
#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>\n", 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\n", type, readok);
exit(0);
}
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap10_code$ gcc statcheck.c csapp.h csapp.c -lpthread -o statcheck
zhaoxiaoan@zhaoxiaoan:~/桌面/Computer_Systems/chap10_code$ ./statcheck abcde.txt
type: regular, read: yes
这个代码块是通过mode参数S_ISREG/S_ISDIR用来检索所输入的文件是什么类型,并且通过参数S_IRUSR来判定该文件是否可读。
读取目录内容
应用程序能够用readdir系列函数来读取目录的内容:
DIR *opendir(const char *name);
它以路径名为参数,返回指向目录流的指针,也就是目录项的列表,如果无下级目录项则返回NULL。
共享文件
- 描述符表:每个进程都有自己独立的描述符表,其表项是由进程打开的文件描述符来索引的,每个打开的描述符表项指向文件表中的一个表项。
- 文件表:打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。文件表表项包括当前的文件位置,引用计数(当前指向该表项的描述符表项数)和一个指向v-node表中对应表项的指针。
- v-node表:所有进程共享这一张表,每个表项包含stat结构体中大多数信息,包括st_mode和st_size成员。
关系图如下所示:
其中第一栏是描述符表,第二栏是共享的打开的文件表,第三栏是共享的v-node表。
I/O部分到此差不多打止了。