一 背景
作为一名Java代码搬运工,我们经常通过java.io包对进行操作IO。由于JVM跨平台的优点,能够帮开发者屏蔽底层细节,使我们不需要直接与操作系统打交道,更加专注业务开发。缺点也很明显,开发者对底层往往一脸懵逼。最近楼主重读了操作系统一书,对底层原理打破沙锅问到底。
二 用户程序读写原理
在Linux操作系统中,内存划分为用户空间和内核空间,在4G内存的32位X86系统中,两者的内存占比是3:1。JVM想读取磁盘中的文件时候,可以直接读取?答案是否定的。硬件设备通常不能与用户空间交互,JVM属于用户进程,驻守在用户空间,所以JVM没法直接读取磁盘中的数据。正确的读流程是:1)用户进程底层使用C语言的read方法进行系统调用; 2)内核向磁盘控制器发送命令,要求其从磁盘读取数据; 3)磁盘控制器通过直接内存读取(DMA),无须CPU协助即可把数据写入内核缓冲区; 4)内核把数据从内核的临时换冲区拷贝到用户空间的缓冲区,如图所示。整个IO过程总结起来就是两次拷贝。
读如此,写亦然,JVM向磁盘中写数据跟读数据流程相反而已。这里不再累述。
三 系统调用函数read和write的API 详解
1)输入函数read
Int n_read = read(int fd, char * buf, int n)
参数fd是文件描述符,从命令行读取数据时,设置为0;buf为读缓冲区;n为每次读取到缓冲区的字节数。
当返回值n_read为-1表示读取失败,大于0表示读到缓存区中的字节数。
2)输出函数write
Int n_written = read(int fd, char * buf, int n)
参数fd是文件描述符,输出到命令行时,设置为1;buf为写缓冲区;n为每次写入缓冲区的字节数。
当返回值n_written为-1表示写入失败,大于0表示成功写入字节数。
通过API我们可以发现,虽然两次拷贝降低IO的读写速率,但是我们可以通过提高每次读出或者写入的数据的字节数,有效提高IO的读写速率。
四 IO模型
C语言系统调用函数read和write是同步阻塞的,即代码执行到read或write方法时候,必须执行完当前代码,才能执行下一步。以读数据为例,read(int fd, char * buf, int n)如果用户空间的buf缓冲区没有读取到n个字节,程序就会一直卡在read这个方法,不会往下执行。正因为如此,所以java.io中读写的方法也是同步阻塞的。
五 系统调用代码
root@ubuntu18:~/c_workspace/io#vim read_write_app.c
//在文件中写入如下代码:
```c
#include <unistd.h>
#define MAXSIZE 1024
int main(void){
char buf[MAXSIZE];
int n;
while((n = read(0, buf, MAXSIZE)))
write(1, buf, n);
return 0;
}
```bash
#编译成二进制可执行文件
root@ubuntu18:~/c_workspace/io# gcc read_write_app.c -o read_write_app
root@ubuntu18:~/c_workspace/io# ls
read_write_app read_write_app.c
#与用户交互
第一个hello world!是用户在shell上输入的,第二个是程序将结果写到shell上。
五 参考文献
1) Brian W. Kernighan,Dennis M.Ritchie著,《C语言程序设计》,第二版本,机械工业出版社
2) Andrew S·Tanenbaum 著,《现代操作系统》,第三版本,机械工业出版社