接上篇 linux IO —— 理解阻塞/非阻塞、同步/异步

linux中,比较常用的IO都是同步的,无论是否被设置为非阻塞IO,都会在一个数据拷贝的阶段等待。

这里列举一下常见的同步IO方式吧。

内核提供的同步IO

打开文件并同步读取

从磁盘读取到内核的高速缓存(若高速缓存中已经有了,直接读取),然后从内核读取到用户空间buffer,通过设置flag o_direct 可以跳过内核缓存直接读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @brief 同步IO:通过linux系统调用 open & read 读取完整文件内容
* @return std::string
*/

std::string ReadBySyscallOpen(){
int fd = open("./test.txt", O_RDONLY);
const int BUF_SIZE = 1024;
char buf[BUF_SIZE];
int size = 0;
std::string s;
do{
size = read(fd, buf, BUF_SIZE);
if(size <= 0) break;
s.append(buf, buf + size);

} while (1);
//printf("%s\n", s.c_str());
close(fd);
return s;
}

只读取数据,不移动指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief pread:只读取数据,不移动指针
*
*/

void PreadBySyscallOpen(){
int fd = open("./test.txt", O_RDONLY);
const int BUF_SIZE = 10;
char buf[BUF_SIZE];
int size = 0;
for(int i=0;i<5;i++){
size = pread(fd, buf, BUF_SIZE, 0);
if(size <= 0) break;
printf("pread: %s\n", buf);
}
close(fd);
}

读取内容到多个 buf 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @brief readv:读取数据到多个buf中,一个填满了就下一个
*
*/

void ReadvBySyscallOpen(){
int fd = open("./test.txt", O_RDONLY);
const int BUF_SIZE = 10;
const int SEQ = 5;
struct iovec iovecs[SEQ];
for(int i=0; i<SEQ; i++){
iovecs[i].iov_base = calloc(1, BUF_SIZE*(i+1));
iovecs[i].iov_len = BUF_SIZE*(i+1);
}
readv(fd, iovecs, SEQ);
for(int i=0;i<SEQ;i++){
printf("readv: %s\n", iovecs[i].iov_base);
}
close(fd);
}

读取内容到多个 buf 中,不移动指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @brief preadv:读取数据到多个buf中,一个填满了就下一个,不移动指针
* 这里有一点要注意的,同一次 preadv 读到的多个buf,还是内容顺序往前读的,只有多次调用 preadv 的时候,文件指针才是不变的
*/

void PreadvBySyscallOpen(){
int fd = open("./test.txt", O_RDONLY);
const int BUF_SIZE = 10;
const int SEQ = 5;
{
struct iovec iovecs[SEQ];
for(int i=0; i<SEQ; i++){
iovecs[i].iov_base = calloc(1, BUF_SIZE*(i+1));
iovecs[i].iov_len = BUF_SIZE*(i+1);
}
preadv(fd, iovecs, SEQ, 0);
//多个 iovec 里边的 buf 数据还是不一样
for(int i=0;i<SEQ;i++){
printf("readv: %s\n", iovecs[i].iov_base);
}
}
//再次调用 preadv,数据又从头开始读了
{
struct iovec iovecs[SEQ];
for(int i=0; i<SEQ; i++){
iovecs[i].iov_base = calloc(1, BUF_SIZE*(i+1));
iovecs[i].iov_len = BUF_SIZE*(i+1);
}
preadv(fd, iovecs, SEQ, 0);
for(int i=0;i<SEQ;i++){
printf("readv: %s\n", iovecs[i].iov_base);
}
}
close(fd);
}

glibc实现的带缓冲同步IO

与直接open、read相比,最重要的区别是,read每次调用都要在用户态→内核态中切换;而 fopen会在用户态创建一个缓冲区,一次性将一个默认大小(一般是一个内核高速缓存页的大小,如4096)的内容拷到用户态缓冲区,每次fgets的时候就不涉及切换了。

可以通过 setvbuf 修改这个缓冲区的大小。

/**
 * @brief 同步IO:通过c标准库glibc的 fopen & fgets & fclose 调用, 通过文件指针 FILE* 来操作文件,底层也是基于系统函数 open 来实现的
 * @return std::string 
 */
std::string ReadByFOpen(){
    FILE* fp= fopen("./test.txt", "r");
    if(fp == nullptr) return "";
    const int BUF_SIZE = 1024;
    char buf[BUF_SIZE];
    int size = 0;
    std::string s;
    while( fgets(buf, BUF_SIZE, fp) != NULL ) {
        //貌似也不需要每次reset,fgets每次读到换行符就结束,而且strlen在最后一波读到结束符就会终止计数
        //memset(buf, 0, BUF_SIZE);
        //printf("buf size: %d\n", strlen(buf));
        //与open+read相比,这里每次只读取了一行,而open+read会读取满指定的buf大小
        s.append(buf, buf+strlen(buf));
    }
    fclose(fp);
    printf("%s\n", s.c_str());
    return s;
}

glibc 还提供了一些其它的同步IO相关的函数,就不一一列举了。

练习代码

练习代码仓库:https://github.com/zouchengzhuo/linux-io-learn/tree/master/4.aio/sync_io

☞ 参与评论