I/O即输入/输出,此日志只讨论Unix I/O
基础知识
- 一切皆文件。所有的I/O设备都被模型化为文件,则所有输入/输出都被当作对相应文件的读和写来执行。
- 描述符。打开文件时内核会返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。
- Linux shell 创建的每个进程开始时都有三个打开的文件: 标准输入(描述符为0)、标准输出(描述符为1)、标准错误(描述符为2)。
- 当前文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。
- 读写文件。一个读操作就是从文件复制若干字节到内存,类似的,写操作就是从内存复制若干字节到文件,从当前文件位置k开始写,并且更新k。
- 文件操作函数
打开或创建文件
int open(char *filename,int flags,mode_t mode);
open函数将filename转换成一个文件描述符,并且返回描述符数字。
flags参数指明指明了进程打算如何访问这个文件:
- O_RDONLY:只读
- O_WRONLY:只写
- O_RDWR:可读可写
- O_CREAT:如果文件不存在,就创建一个空文件
- O_TRUNC:如果文件存在,则清空它
- O_APPEND:每次写操作前,设置文件位置到文件结尾处
mode指定新文件的访问权限位,具体不再详说。
关闭文件
int close(int fd);
fd为文件描述符
读写文件
ssize_t read(int fd,void *buf,size_t n);
ssize_t write(int fd,const void *buf,size_t n);
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示实际传送的字节数量。
write函数从内存位置buf复制最多n个字节到描述符为fd的当前文件位置。
实例分析
在运行下列代码前需要导入csapp.c和csapp.h文件,需与下列代码文件存在同一目录下。abcde.txt中存有字符串“abcde”。
实例一
#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;
}
编译运行
在编译之前需要进行链接
运行结果
知识点介绍
dup2函数:
int dup2(int oldfd,int newfd);
-
dup2函数复制描述符表表项oldfd到描述符表项newfd,覆盖描述符表项newfd以前的内容。
-
可以通过下面这个例子加强理解
在调用dup2之前的状态:
调用dup2(4,1)之后:
代码分析
-
第一次打开abcde.txt后,文件描述符为fd1,当前文件位置为0,read函数读取第一个字节,故c1=a。
-
第二次打开abcde.txt后,文件描述符为fd2,当前文件位置依旧为0(相当于打开了一个新文件),read函数读取第一个字节,并更新当前文件位置为1,故c2=a。
-
第三次打开abcde.txt后,文件描述符为fd3,但dup2函数使fd3指向了fd2指向的文件,所以read函数从当前文件位置为1开始读取第二个字节,故c3=b。
实例二
#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;
}
编译运行
知识点介绍
- fork父进程运行时getpid函数返回子进程的pid,子进程运行时返回0。
- sleep函数可以让进程休眠指定的秒数。
代码分析
- 第一个read函数读取文件中第一个字节,并更新当前文件位置,故c1=a。
- 调用fork函数后父进程和子进程并发进行。子进程复制了父进程的环境。
- 程序先进入父进程(也有先进入子进程的可能性),read函数读取第二个字节,并更新当前文件位置,故此时c2=b。后进入子进程中,子进程复制了父进程的文件表表项,read函数从当前文件位置开始读取第三个字节,故此时c2=c。
实例三
#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);
Write(fd2, "wxyz", 4);
Write(fd3, "ef", 2);
Close(fd1);
Close(fd2);
Close(fd3);
return 0;
}
编译运行
命令行
运行结果
知识点介绍
- 创建一个文件,若原文件存在,则清空它,代码如下:
fd1 = Open(fname, O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR);
- 向文件中添加字节,打开文件后当前文件位置为文件中所有字节末尾,代码如下:
fd3 = Open(fname, O_APPEND|O_WRONLY, 0);
- 此代码效果等同于dup2(1,2)
fd2 = dup(fd1);
代码分析
- 第一次打开文件后,文件描述符为fd1,write函数将“pqrs”字符串写进了文件,并更新了当前文件位置。
- 第二次打开文件后,文件描述符为fd3,当前文件位置为所有字节末尾,故write函数在“pqrs”字符后添加了“jklmn”。
- 调用dup函数后,fd2指向了fd1所指文件,故write函数从“s”字符后开始写,“wxyz”覆盖了“jklm”。
- fd3所指文件的当前文件位置为末尾,故最后一个write字符从末尾开始写,将“ef”添加进文件中。
- 故最后文件中的字符串为“pqrswxyznef”。