IO子系统层次结构
从用户I/O软件切换到内核I/O软件的唯一 办法是“异常”机制:系统调用(自陷)
大部分I/O软件都属于操作系统内核态程序,最初的I/O请求在用户程序中提出。
OS在I/O子系统中的重要性由I/O系统以下三个特性决定
- 共享性。I/O系统被多个程序共享,须由OS对I/O资源统一调度管理,以保证用户程序只能访问自己有权访问的那部分I/O设备。
- 复杂性。I/O设备控制细节复杂,需OS提供专门的驱动程序进行控制,这样可对用户程序屏蔽设备控制的细节。
- 异步性。不同设备之间速度相差较大,因而,I/O设备与主机之间的信息交换使用异步的中断I/O方式,中断导致从用户态向内核态转移,因此必须由OS提供中断服务程序来处理。
文件
记住:LInux中一切皆文件。
- ASCII文件。ASCII文件也称文本文件,可由多个正文行组成,每行以换行符‘\n’ 结束
- 二进制文件。
- 标准输入和标准(错误)输出文件是ASCII文件。
- 普通文件可能是文本文件或二进制文件。
- 不同操作系统判断一行结束的符号不同。Linux & Mac OS: \n(0xa),Windows & 网络协议: \r\n (0xd 0xa)
- 打开文件:int open(char *name, int flags, mode_t perms);
文件描述符 file descriptor
从当前未使用的最小的分配(3开始)
打开/关闭文件
int open(char *name, int flags, mode_t perms); /*打开文件*/
//e.g. fd=open(“test.txt”,O_RDONLY, 0);
/*
参数flags:O_RDONLY, O_WRONLY|O_APPEND, O_RDWR等
参数perms:用于指定文件的访问权限,通常在open函数中该参数总是0,除非以创建方式打开,此时,参数flags中应带有O_CREAT标志。
return:文件描述符
*/
-
标准输入(fd=0)、标准输出(fd=1)和标准错误(fd=2)三种文件在由shell创建进程时默认打开,其他文件须用creat或open函数显式创建或打开后才能读写
Example
int fd; // 文件描述符 file descriptor
if ((fd = open("/etc/hosts", O_RDONLY)) < 0)
{
perror("open");
exit(1);
}
int fd; // 文件描述符
int retval; // 返回值
int ((retval = close(fd)) < 0)//如果在此关闭已经关闭了的文件,会出大问题。所以一定要检查返回值
{
perror("close");
exit(1);
}
读写文件
ssize_t read(int fd, void *buf, size_t n);/*从文件中读取n个元素到buf中,返回读取字节数*/
ssize_t write(int fd, const void * buf, size_t n);/*从buf中写n个文件到fd标识的文件*/
/*size_t 和 ssize_t 分别是 unsigned int 和 int,因为返回值可能是-1。*/
Example
char buf[512];
int fd;
int nbytes;
/* 打开文件描述符,并从中读取 512 字节的数据,实际上就是把文件中对应的字节复制到内存中
返回值是读取的字节数量,是一个 ssize_t 类型(其实就是一个有符号整型),如果 nbytes < 0 那么表示出错。nbytes < sizeof(buf)
这种情况(short counts) 是可能发生的,而且并不是错误。*/
if ((nbytes = read(fd, buf, sizeof(buf))) < 0)
{
perror("read");
exit(1);
}
设置读写位置
long lseek(int fd, long offset, int origin);
/*offset指出相对字节数
origin指出基准:开头(0)、当前位置(1)和末尾(2)
返回的是位置值,若发生错误,则返回-1
例:lseek(fd,5L,0);表示定位到文件开始后的第5字节
lseek(fd, 0L, 2);表示定位到文件末尾*/
重定向
内核用3个相关的数据结构来表示打开的文件:
- 描述符表。每个进程有独立的描述符表每个描述符表项指向文件表一个表项
- 文件表。所有进程共享。
- v-node表
- 每个描述符都有他自己的文件位置,所以可以两个打开的文件v-node一样,但其实是打开了两个(操作独立)。
- 父子进程指向同一个打开表表项(即是对同一个打开文件操作)
dup2(int old,int new)
复制描述符表项old到new,则new与old指向同一个文件,对文件操作同步
链接命令
gcc csapp.h csapp.c io.c -lpthread -o io
Example1:
打开同一个文件:
#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;
}
/*
c1=a,c2=a,c3=b
*/
虽然打开的是同一个文件,但fd1、fd2、fd3各不相同,指向的文件表各不相同。第一个Read,从fd1指向文件头处读取1个字节给c1,第二个Read,从fd2指向文件头处读取1个字节给c2,此时光标后移一位。dup重定位后fd3指向了fd2指向的文件表,所以这时的Read是接着刚刚对fd2文件的操作,从第一个字节之后再读一个字节赋给c3
Example2
父子进程
#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;
}
/*
Parent: c1 = a, c2 = b
qian64@qian64-VirtualBox:~/csapp$ Child: c1 = a, c2 = c
*/
Example3
写重定向
#include "csapp.h"
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;
}
/*abcde.txt
pqrswxyznef
*/
开始对fd1 Write(fd1, "pqrs", 4)操作后文件为:pqrs
由于fd3设置O_APPEND,所以Write(fd3, "jklmn", 5)之后文件为pqrsjklmn
fd2重定向到fd1指向的文件表,而fd1当前光标在第5个位置,所以Write(fd2, "wxyz", 4);后变为:pqrswxyzn
之后Write(fd3, "ef", 2);追加两个字符后为:pqrswxyznef