Linux的基础I/O
目录
1、理解“文件”
1.1 狭义理解
1.2 广义理解
1.3 文件操作的归类认知
1.4 系统角度
2、回顾C文件接口
2.1 文件的打开与关闭
2.2 文件的读写函数
2.3 stdin & stdout & stderr
3、系统文件I/O
3.1 一种传标志位的方式
3.2 文件的系统调用接口
3.2.1 open()
3.2.2 read() & write() & close()
3.3 库函数和系统调用
3.4 文件描述符fd
3.4.1 0 & 1 & 2
3.4.2 文件描述符的分配规则
3.4.3 重定向
3.4.4 重定向系统调用dup2()
4、理解一切皆文件
5、缓冲区
5.1 缓冲区的定义
5.2 缓冲区的作用
5.3 缓冲区的机制
现象1:
现象2:
1、理解“文件”
1.1 狭义理解
- 文件在磁盘里。
- 磁盘是永久性存储介质,因此文件在磁盘上永久性存储。
- 磁盘是外设(即是输出设备也是输入设备)。
- 对磁盘文件的所有操作(如读取、写入)本质上都是对外设的输入/输出,简称I/O(Input/Output)。
1.2 广义理解
- Linux中,一切皆文件(键盘、显示器、网卡、磁盘……这些都是抽象化的过程)(后面会深入理解)。
1.3 文件操作的归类认知
- 文件 = 属性(元数据)+ 内容。
- 对于0KB的空文件是占用磁盘空间的,有文件属性。
- 所有的文件操作本质是文件内容操作和文件属性操作。
1.4 系统角度
- 对文件的操作本质是进程对文件的操作。
- 磁盘的管理者是操作系统。
- 文件的读写本质不是通过C语言/C++的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的。
文件分为“内存级(被打开)”文件,“磁盘级(未打开)”文件。
本节主讲“内存级(被打开)”文件。
2、回顾C文件接口
2.1 文件的打开与关闭
FILE *fopen(const char *path, const char *mode);
\"r\"
NULL
\"r+\"
NULL
\"w\"
\"w+\"
\"a\"
\"a+\"
int fclose(FILE *fp);
注意:
ls /proc/[ 进程 id] -l 命令,查看当前正在运行进程的信息。
- cwd:指向进程的当前工作目录,创建文件和打开文件的默认路径。
- exe:指向启动当前进程的可执行文件的路径。
2.2 文件的读写函数
(如
stdin
、文件)FILE *stream
(文件指针)int
)失败返回
EOF
(如
stdout
、文件)int char
(字符)FILE *stream
int
)失败返回
EOF
char *str
(缓冲区)int n
(最大长度)FILE *stream
str
失败返回
NULL
\\n
const char *str
(字符串)FILE *stream
失败返回
EOF
scanf
)FILE *stream
const char *format
(格式字符串)...
(变量地址)失败返回
EOF
printf
)FILE *stream
const char *format
...
(变量值)失败返回负值
void *ptr
(缓冲区)size_t size
(每块大小)size_t nmemb
(块数)FILE *stream
const void *ptr
(数据地址)size_t size
size_t nmemb
FILE *stream
注意:
写字符串,不用写\\0,因为这是C语言的规定,不是文件的规定,写进去会乱码。
2.3 stdin & stdout & stderr
C程序启动,默认打开三个输入输出流,分别是stdin,stdout,stderr。
#include extern FILE *stdin; // 标准输入,键盘文件extern FILE *stdout; // 标准输出,显示器文件extern FILE *stderr; // 标准错误,显示器文件
3、系统文件I/O
3.1 一种传标志位的方式
使用位图,用比特位作为标志位。
#include #define ONE (1 << 0) // 0000 0001 (二进制)#define TWO (1 << 1) // 0000 0010 (二进制)#define THREE (1 << 2) // 0000 0100 (二进制)void func(int flags) { if (flags & ONE) printf(\"flags has ONE! \"); if (flags & TWO) printf(\"flags has TWO! \"); if (flags & THREE) printf(\"flags has THREE! \"); printf(\"\\n\");}int main() { func(ONE); // 输出: flags has ONE! func(THREE); // 输出: flags has THREE! func(ONE | TWO); // 输出: flags has ONE! flags has TWO! func(ONE | TWO | THREE); // 输出: flags has ONE! flags has TWO! flags has THREE! return 0;}
3.2 文件的系统调用接口
man 2 系统调用,有具体说明。
3.2.1 open()
#include #include #include int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);
pathname:要打开或创建的目标文件路径。
flags:打开文件时的选项标志,可以使用以下常量通过\"或\"运算(|)组合:
必须指定且只能指定一个的选项:
-
O_RDONLY:只读打开。
-
O_WRONLY:只写打开。
-
O_RDWR:读写打开。
可选标志:
-
O_CREAT:若文件不存在则创建它(需要mode参数,设置新文件的访问权限)。
-
O_APPEND:追加写模式。
-
O_TRUNC:如果文件已存在且为普通文件,打开时会将其长度截断为0,逻辑上的清空(类似与vector的size)
return value:
-
成功:返回新打开的文件描述符fd(非负整数)
-
失败:返回-1。
注意:
那么C语言的fopen的flag就是:
“r” = O_RDONLY;
“w” = O_CREAT | O_WRONLY | O_TRUNC;
“a” = O_CREAT | O_WRONLY | O_APPEND;
“r+” = O_RDWR;
“w+” = O_CREAT | O_RDWR | O_TRUNC;
“a+” = O_CREAT | O_RDWR | O_APPEND。
3.2.2 read() & write() & close()
类比C文件相关接口。
#include ssize_t read(int fd, void *buf, size_t count);fd:文件描述符buf:存储读取数据的缓冲区count:请求读取的字节数
#include ssize_t write(int fd, const void *buf, size_t count);fd:文件描述符buf:包含待写入数据的缓冲区count:请求写入的字节数
#include int close(int fd);fd:要关闭的文件描述符
注意:
read()和write()的buf都是void*,不关心数据格式,以二进制流输入输出。
那么为什么语言层,有字符流的输入输出?
- 首先,底层都是二进制流的输入输出。
- 字符按ASCII输入(读出),按ASCII输出(写入)。对于字符设备,字符通过ASCII转化成二进制写到里面,然后通过ASCII解释,以字符的形式显示。
字符流的输入输出,是因为,我们输入输出的是字符串。
3.3 库函数和系统调用
fopen
, fclose
, fread
, fwrite
2. 带缓冲区
3. 可移植性更好
4. 最终会调用系统调用
open
, close
, read
, write
, lseek
2. 无缓冲区
3. 效率更高但更底层
4. 与具体操作系统相关
3.4 文件描述符fd
3.4.1 0 & 1 & 2
Linux 进程默认情况下会有 3 个缺省打开的文件描述符,分别是
标准输入 0,标准输出 1,标准错误 2
0,1,2 对应的物理设备一般是:键盘,显示器,显示器
所以输入输出还可以采用如下方式:
0,1,2是自动打开的
#include #include #include #include #include #include // 添加read/write所需的头文件int main(){ char buf[1024]; // 从标准输入(文件描述符0)读取数据 ssize_t s = read(0, buf, sizeof(buf) - 1); // 保留1字节给结尾的\\0 if(s > 0) { buf[s] = \'\\0\'; // 添加字符串结束符 // 将输入内容同时输出到标准输出(1)和标准错误(2) write(1, buf, s); write(2, buf, s); } return 0;}
而现在知道,文件描述符就是从 0 开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了 file 结构体,表示一个已经打开的文件对象。而进程执行 open 系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针 * files,指向一张表 files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
注意:
C语言的stdin(fd = 0),stdout(fd = 1),stderr(fd = 2),是一个FILE结构体的指针,FILE结构体里面封装了文件描述符fd,其他语言也一样。
3.4.2 文件描述符的分配规则
直接看代码:
#include #include #include #include #include // 添加 close 函数所需的头文件int main(){ int fd = open(\"myfile\", O_RDONLY); if (fd < 0) { perror(\"open\"); return 1; } printf(\"fd: %d\\n\", fd); close(fd); // 正确的关闭位置 return 0;}
输出: fd: 3
关闭 fd = 0 或者 fd = 2,再看
#include #include #include #include #include // 添加 close() 所需的头文件int main(){ close(0); // 关闭标准输入(文件描述符 0) // close(2); // 注释掉的关闭标准错误(文件描述符 2) int fd = open(\"myfile\", O_RDONLY); if (fd < 0) { perror(\"open\"); return 1; } printf(\"fd: %d\\n\", fd); close(fd); // 关闭文件描述符 return 0;}
输出: fd: 0或 fd: 2
结论:
在 Linux 系统中,文件描述符的分配原则:最小的,没有被使用的下标,作为fd,给新打开的文件。
3.4.3 重定向
那如果关闭 fd = 1 呢?看代码:
#include #include #include #include #include int main(){ close(1); int fd = open(\"myfile\", O_CREAT | O_WRONLY | O_TRUNC, 0644); if(fd < 0) { perror(\"open\"); return 1; } printf(\"fd: %d\\n\", fd); fflush(stdout); close(fd); exit(0);}
因为语言层只认stdout中的fd = 1,此时下标为1的指针指向myfile,所以
本来应该输出到显示器上的内容,输出到了myfile文件中。
这种现象叫做输出重定向。
常见的重定向有: >,>>,<。
输出重定向的本质:
注意:
cat log.txt > myfile,实际上是cat log.txt 1>myfile,只重定向了标准输出,
cat log.txt 1>myfile 2>&1,重定向了标准输出和标准错误。
3.4.4 重定向系统调用dup2()
#include int dup2(int oldfd, int newfd);
oldfd的指针 覆盖 newfd的指针 。
如:dup2(fd,0),实现输入重定向,dup2(fd,1),实现输出重定向。
所以,重定向 = 文件打开方式 + dup2()。
4、理解一切皆文件
首先,在 Windows 中是文件的东西,它们在 Linux 中也是文件;其次一些在 Windows 中不是文件的东西,比如进程、磁盘、显示器、键盘这样的硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至管道,也是文件;将来我们要学习网络编程中的 socket(套接字)这样的东西,使用的接口跟文件接口也是一致的。
这样做最明显的好处是,开发者仅需要使用一套 API ,即可调取 Linux 系统中绝大部分的资源。举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读 PIPE)的操作都可以用 read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以用 write 函数来进行。
上图中的外设,每个设备都可以有自己的 read、write,但一定是对应着不同的操作方法!!但通过 struct file 下的 struct file_operations 中的各种函数回调,让我们开发者只用 file 便可调取 Linux 系统中绝大部分的资源!!这便是 \"Linux 下一切皆文件\" 的核心理解。
多态的体现。
5、缓冲区
5.1 缓冲区的定义
临时存储数据的内存区域。
5.2 缓冲区的作用
提高使用者的效率。
5.3 缓冲区的机制
- 用户级语言层缓冲区,避免频繁调用系统调用(成本高),提高C语言接口的效率。
- 文件内核缓冲区,提高系统调用的效率。
- 可以通过fsync(),将文件内核缓冲区的数据刷新到硬件。
- 一般认为数据交给OS,就相当于交给硬件。
基于上面的机制,可以理解下面的现象:
现象1:
#include #include #include #include #include #include int main() { // 关闭标准输出(文件描述符1) close(1); int fd = open(\"log.txt\", O_CREAT | O_WRONLY | O_TRUNC, 0664); if (fd < 0) { perror(\"open\"); return 1; } printf(\"hello world: %d\\n\", fd); // 注意:这里打印的fd值应该是1 close(fd); return 0;}
这个时候,对于普通文件,应该是满了刷新,可是没满,也没有强制刷新,然后关闭了fd,在程序退出时,刷新,但fd已经关闭了,刷新不了,所以log.txt中不会有数据。
可以使用 fflush() 强制刷新下缓冲区。
#include #include #include #include #include #include int main() { // 关闭标准输出(文件描述符1) close(1); int fd = open(\"log.txt\", O_CREAT | O_WRONLY | O_TRUNC, 0664); if (fd < 0) { perror(\"open\"); return 1; } printf(\"hello world: %d\\n\", fd); // 注意:这里打印的fd值应该是1 fflush(stdout); // 强制刷新 close(fd); return 0;}
注意:stderr是不带缓冲区,即立即刷新。
现象2:
#include #include #include // 添加 write() 和 fork() 所需的头文件int main() { const char *msg0 = \"hello printf\\n\"; const char *msg1 = \"hello fwrite\\n\"; const char *msg2 = \"hello write\\n\"; printf(\"%s\", msg0); fwrite(msg1, 1, strlen(msg1), stdout); write(1, msg2, strlen(msg2)); fork(); return 0;}
结果:
hello printfhello fwritehello write
显示器,行刷新;
系统调用write(),直接写入内核。
但是重定向一下 ./hello > file,结果:
hello writehello printfhello fwritehello printfhello fwrite
系统调用write(),直接写入内核;
重定向,改变了刷新方式,普通文件,满了刷新,可是没慢,也没有强制刷新,程序退出时,刷新,父子进程各刷新一份。