Unix I/O
一切皆文件==
所有的I/O设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅的映射为文件的方式,允许linux内核引出一个简单、低级的应用接口,称为Unix I/O.
一个应应用程序通过要求内核打开相应文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符。
Linux shell创建的每个进程开始都有三个打开的文件:
标准输入(描述符为0)、标准输出(描述符为1)、标准错误(描述符为2)
头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO用他们来代替描述符值。
打开和关闭文件
用open函数打开一个已经存在的文件或创建一个新文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char* filename,int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是进程中当前没有打开的最小描述符。又shell已经打开了3个标准文件故描述最小为3.
函数的3个参数
filename文件名
flags参数指明了打开文件的方式:
O_RDONLY:只读
O_WRONLY: 只写
O_RDWR: 可读可写
O_CREAT: 文件不存在,就创建一个它的截断的空文件。(就是创建一个新空白文件)
O_TRUNC: 如果文件已经存在,就截断它。(清空它)
O_APPEND: 在每次写操作前,设置文件位置到文件结尾处。
mode参数指定了访问权限:
S_IRUSR:使用者(拥有者)能够读这个文件
S_IWUSR:使用者(拥有者)能够写这个文件
S_IXUSR:使用者(拥有者)能够执行这个文件
S_IRGRP:拥有者所在组的成员能够读这个文件
S_IWGRP:拥有者所在组的成员能够写这个文件
S_IXGRP:拥有者所在组的成员能够执行这个文件
S_IROTH:其他人(任何人)能够读这个文件
S_IWOTH:其他人(任何人)能够写这个文件
S_IXOTH:其他人(任何人)能够执行这个文件
一般S_IRUSR、S_IWUSR、S_IXUSR为4、2、1可用这几个数字相加来组合权限,如6表示使用者可读可写这个文件
用close函数关闭一个打开的文件
#include <unistd.h>
int close(int fd);
读和写文件
#include <unistd.h>
ssize_t read(int fd,void *buf,size_t n);
ssize_t write(int fd,const void *buf,size_t n);
read函数:fd表示文件描述符 buf指向一段内存空间表示读到哪去 size表示读多少字节
write函数从内存buf出至多复制n个字节到描述符为fd的当前文件位置。
读取文件元数据
应用程序能够通过调用stat和fstat函数,检索到关于文件的信息(有时也称为文件的元数据)
#include <unistd.h>
#include <sys/stat.h>
int stat(const char*filename,struct stat* buf);
int fstat(int fd,struct stat* buf);
stat函数以一个文件名为输入,可以查看到文件的各种数据,fstat函数是相似的。
st_mode成员的文件类型:
S_ISREG(m)。这是一个普通文件吗?
S_ISDIR(m)。这是一个目录文件吗?
S_ISSOCK(m)。这是一个网络套接字吗?
下面程序查询和处理一个文件的st_mode位:
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);
}
共享文件
内核用下面三个相关的数据结构来表示打开的文件:
描述符表
每个进程都有独立的描述符表,表项是由进程打开的文件描述符来索引的。每个描述符表项只想文件表中的一个表项。
文件表
打开文件的集合是由一张文件表来表示的,所有进程共享。它记录了当前文件的位置,当前指向该表项的描述符表项数(成为引用计数)和一个指向v-node表中对应表项的指针。当引用计数为0是,内核会自动删除这个文件表表项。
v-node表
所有进程共享,包含了stat结构中的大多数信息。
两个描述符引用不同的文件。没有共享
两个描述符通过两个打开文件表表项共享同一个磁盘文件
子进程继承父进程的打开文件(初始状态如上上图)
看代码:
#include "csapp.h"
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;
}
运行结果
我们看到了:
1.从abcde.txt,并且读取了一个字符,现在光标停留在了a之后
2.进行了fork,子进程复制了父进程的环境。
3.然后两个进程都遇到了sleep,接着先继续执行子进程,它读了一个字符,因为先前光标停留在ab之间,所以此时c2读到的是b
4.child执行完后回到父进程,父进程和子进程的文件表表项相同,所以光标在b后,往后读一个,读到了c
I/O重定向
Linux shell提供了I/O重定向操作符,允许用户将磁盘文件和标准输入输出联系起来。如:
linux >ls>foo.text使得shell加载ls程序,将标准输出重定向到foo.text.
如何重定向:
使用dup2函数
#include <unistd.h>
int dup2(int oldfd,int newfd);
dup2函数复制描述符表表项oldfd到描述符表项newfd,覆盖描述符表项newfd以前的内容。如果newfs已经带开了,dup2会在复制oidfd之前关闭newfd.
看代码示例;
#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;
}
运行结果;
fd1正常读写abcde.text的内容读到a;
fd2也正常读写abcde.text的内容读到a,光标在ab间,
fd3重定位到fd2顺着继续读写一个字符得到b。