博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

高级IO函数

Posted on 2016-03-23 16:04  bw_0927  阅读(306)  评论(0)    收藏  举报

http://www.zyfforlinux.cc/2014/11/16/%E9%AB%98%E7%BA%A7IO%E5%87%BD%E6%95%B0%E8%AF%A6%E8%A7%A3/

 

高级IO函数概述

  • char buf1[MAXSIZE] char buf2[MAXSIZE]有这样两块内存数据怎么样一次性写入到描述符(文件描述符/socket描述符等等)中
  • read/write函数的时候,如果读取/写入的数据小于你所期望的时候,并且不是因为达到文件尾端,这不是一个错误,应该让read/write继续读取数据
  • 如何在描述符之间直接拷贝数据,避免将数据从用户空间拷贝到内核空间。
  • 如何在两个管道描述符之间传递数据。

注:web server访问某一个文件的时候流程是这样的
普通的直接IO方式:

1
2
read(file, tmp_buf, len);
write(socket, tmp_buf, len);

步骤一:系统调用read导致了从用户空间到内核空间的上下文切换。DMA模块从磁盘中读取文件内容,并将其存储在内核空间的缓冲区内,完成了第1次复制
步骤二:数据从内核空间缓冲区复制到用户空间缓冲区,之后系统调用read返回,这导致了从内核空间向用户空间的上下文切换。
此时,需要的数据已存放在指定的用户空间缓冲区内(参数tmp_buf),程序可以继续下面的操作。
步骤三:系统调用write导致从用户空间到内核空间的上下文切换。数据从用户空间缓冲区被再次复制到内核空间缓冲区,完成了第3次复制。
不过,这次数据存放在内核空间中与使用的socket相关的特定缓冲区中,而不是步骤一中的缓冲区。
步骤四:系统调用返回,导致了第4次上下文切换。第4次复制在DMA模块将数据从内核空间缓冲区传递至协议引擎的时候发生.

一个简单的read write居然做了这么多事情,但是你会发现其中步骤二和步骤三是可以省去的,那么sendfile就去掉了一些不必要的数据拷贝操作。所以使用
sendfile机制是高效的,但不是觉得的。下面会详细介绍相关的概念

readv/writev

1
2
3
4
5
6
7
8
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
iovec是一个结构体用来存放多块内存区域的地址和大小信息
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};

上面这两个系统调用就可以实现,多块不连续的内存集中写和读。
下面使用一个小列子来演示下这两个系统调用的功能。

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
//结合一种场景来描述
/*
* 在web server中通常需要响应一个http head和 http body,通常http head是通用的
* 所以http head单独使用一个buf存放。那么可以使用writev来实现对http head和body一次性发送给用户
* 这只是一个比较常用的一种场景。
*/
#include <sys/uio.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
//将两块不是连续的内存区域的数据写入文件

int main()
{
struct iovec data[2];
char buf1[] = "buf1";
char buf2[] = "buf2";
int fd = open("1.txt",O_CREAT|O_EXCL|O_RDWR,0777);
if(fd < 0)
{
//文件已经存在
fd = open("1.txt",O_RDWR);
}
data[0].iov_base = buf1;
data[0].iov_len = strlen(buf1);
data[1].iov_base = buf2;
data[1].iov_len = strlen(buf2);
writev(fd,data,2);
close(fd);
}

上面使用writev实现了对buf1和buf2这两个不连续的内存块进行集中写入,如果没有这个函数那我们就需要调用两次api,如果有多块数据呢,那就要调用多次,
这样系统调用的上下文切换开销就大多了。

readn/writen

readn和writen其实系统并没有提供给我们,只是为了让read和write更加健壮,很多书中都有readn和writen的具体实现。主要是避免一种情况那就是read/write被信号打断。
当read/write的时候如果有信息发生,那么read/write就会立即返回,返回以及读入或者写入的数据.但是这通常不是我们所期望的,因为我想读取或者写入N个字节,但是因为被信号打断
导致实际上没有写入或者读取N个字节,那么就需要再次调用read/write继续完成剩下的工作,那么此时readn和writen就派上用场了。
下面的代码就是两者的实现

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
35
36
/*
* readn实现,避免被信号打断。直到读取到指定大小的数据后才返回
*/

#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
ssize_t readn(int fd,void *vptr,size_t n)
{
size_t nleft;
size_t nread;
char *ptr;
ptr = (char*)vptr;
nleft = n;
while(nleft > 0){
if((nread = read(fd,ptr,nleft)) < 0) {
if(errno == EINTR)
nread = 0; //如果被信号打断,重新读取
else //read不是因为信号而发生的错误
return -1;
}else if(nread == 0)
break; //EOF 文件读取结束
nleft -= nread; //减去读过的
ptr += nread; //buf指针前移
}
return (n-nleft); //返回读取到多少字节数据
}

int main()
{
char buf[100];
printf("%d\n",readn(0,buf,100));
}
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
35
36
37

/*
* writen实现,避免被信号打断。直到读取到指定大小的数据后才返回
*/

#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
ssize_t writen(int fd,void *vptr,size_t n)
{
size_t nleft;
size_t nwritten;
const char *ptr;
ptr = (const char*)vptr;
nleft = n;
while(nleft > 0){
if((nwritten = write(fd,ptr,nleft)) <= 0) {
if(nwritten < 0 && errno == EINTR)
nwritten = 0; //如果被信号打断,重新读取
else
return -1; //writen不是因为信号而发生的错误
}
nleft -= nwritten; //减去已经写入的字节
ptr += nwritten; //指针前移
}
return (n-nleft); //返回实际写入多少字节
}

int main()
{
char buf[] = "zhangyifei";
printf("%d\n",writen(1,buf,strlen(buf)));
}

sendfile系统调用

在概述部分说道传统的read/write在两个描述符中传递数据是很耗费时间的,需要经过四次的复制操作。
在介绍sendfile系统的时候不得不去说mmap系统调用,通过使用mmap系统调用可以减少一次复制操作,使用mmap系统调用.
关于mmap系统调用的使用方法可以参见我的博文中关于进程间通信的那篇文章)后就将文件映射到内核缓冲区了,那么
在概述部分描述的read/write传统方式中的步骤二就省去了,不需要将内核缓冲区的数据再次拷贝到用户空间了。通过调用 mmap() 而不是 read()
,我们已经将内核需要执行的复制操作减半。当有大量的数据传输时,有相当好的效果。但是性能改进的同时,
也潜藏着一定的代价与陷阱。比如,在对文件进行内存映射后调用 write()
,而这时有另外一个进程将映射的文件截断,此时 write() 系统调用会被进程接收到的 SIGBUS 信号而中断,
SIGBUS 信号往往意味着尝试进行非法地址访问。对 SIGBUS
信号的默认处理方式是杀死当前进程并生成 core dump 文件 — 这对于网络服务器来说是极不期望的!
mmap方式:

1
2
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

有两种方式可以解决该问题:

第一种是为 SIGBUS 信号设置处理程序,并在处理中简单的执行 return 语句。这样,write() 系统调用将返回被信号中断前已写的字节数,同时设置 errno 变量。但是这样的做法并不值得鼓励,因为收到 SIGBUS 信号意味着发生了严重错误!

第二种是采用文件租约的方式。文件租约是指,通过对文件描述符执行租借,你可以和内核对某个文件达成租约,从内核可以获得读/
写租约。当另外的一个进程试图将你正在传输的文件截断时,内核会向你发出实时信号 RT_SIGNAL_LEASE 。
该信号通知你的进程,内核即将终止你在该文件上曾经获得的租约。这样,当
write() 访问非法地址时,并即被随后到来的 SIGBUS 杀诉之前,write() 系统调用会被 RT_SIGNAL_LEASE 信号中断。
write() 的返回值就是被中断之前已写的字节数,全局变量 errno
设置为成功。下面是一段示例租约的代码:

1
2
3
4
5
6
7
8
9
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) { //设置描述符触发 RT_SIGNAL_LEASE信号
perror("kernel lease set signal");
return -1;
}
/* l_type can be F_RDLCK F_WRLCK */ //设置租约的类型,F_EDLCK 只读,和F_WRLCK 读写
if(fcntl(fd, F_SETLEASE, l_type)){
perror("kernel lease set type");
return -1;
}

使用 mmap 是 POSIX 兼容的,但是使用 mmap 并不一定能获得理想的数据传输性能。数据传输的过程中仍然需要一次 CPU
拷贝操作,而且映射操作也是一个开销很大的虚拟存储操作,这种操作需要通过更改页表以及冲刷 TLB (使得 TLB
的内容无效)来维持存储的一致性。但是,因为映射通常适用于较大范围,所以对于相同长度的数据来说,映射所带来的开销远远低于 CPU 拷贝所带来的开销。

sendfile() 的目的是简化通过网络在两个本地之间的数据传输过程。sendfile() 系统调用的引入,不仅减少了数据的复制,还减少了上下文切换的次数。 使用方法如下:

1
2
3
4
5
6
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
1. out_fd 待写入的文件描述符
2. in_fd 待读出内容的描述符
3. offset 指定从读入文件流的哪个位置开始读
4. 读取多少字节的数据
  • sendfile() 导致文件内容通过 DMA 模块复制到某个内核缓冲区,
  • 然后被复制到与 soket 相关联的的缓冲区中。
  • 最后DMA 模块将 socket 缓冲区中的数据复制到协议亲引擎,这时进行第 3 次复制。

在调用 sendfile() 期间,如果有另外一个进程将文件截断,且进程没有为 SIGBUS 注册任何的信号处理函数时,
sendfile() 调用仍会返回进程被信号中断前已发送的字节数,并将全局变量 errno 设置为成功。然而,类似的,
如果在调用 sendfile() 前,从内核里获得了文件租约,那么 sendfile() 在返回前也会收到 RT_SIGNAL_LEASE。

sendfile常常被用于在文件描述符和socket描述符之间拷贝数据
下面是一个使用sendfile系统调用在两个文件描述符直接拷贝数据的例子:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
* sendfile系统调用
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/sendfile.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main()
{
char buf[BUFSIZ];
int n=0;
const char *filename1 = "in.txt";
const char *filename2 = "out.txt";
int fd = open(filename1,O_RDWR);
int fd2 = open(filename2,O_RDWR);
struct stat file_buf;
fstat(fd,&file_buf);
sendfile(fd2,fd,NULL,file_buf.st_size);
// while((n = read(fd,buf,BUFSIZ)) > 0)
// {
// write(fd2,buf,BUFSIZ);
// }
// if(n == 0)
// printf("cp complete\n");
// else
// printf("cp failue\n");

}

上面这段程序是使用两种方式对文本进行拷贝操作,本次实验对一个48M大小得到文本进行拷贝操作,

普通的直接拷贝结果是:
cp complete

real 0m1.875s
user 0m0.003s
sys 0m0.266s
可以发现大部分的时间花在了sys,也就是内核空间,因为数据的复制拷贝工作是发生在内核空间,并且read/write多次进行了上下文切换

使用sendfile系统调用的结果:

real 0m0.100s
user 0m0.002s
sys 0m0.053s
你会发现sendfile系统调用用了更少的时间花在内核空间中,如果你用小文件测试,你并不会发现有多大的差别。毕竟sendfile只是减少了从内核空间拷贝数据
到用户空间的那一次复制

splice系统调用

splice其实也是在两个文件描述符直接拷贝数据不需要在内核空间和用户空间之间拷贝数据
splice本身也是实现了零拷贝的操作,与 sendfile() 不同的是,splice()
允许任意两个文件之间互相连接(其中一个必须是管道),而并不只是文件到 socket 进行数据传输。对于从一个文件描述符发送数据到
socket 这种特例来说,一直都是使用 sendfile() 这个系统调用,而 splice 一直以来就只是一种机制,
它并不仅限于 sendfile() 的功能。也就是说,sendfile() 只是 splice()
的一个子集,在 Linux 2.6.23 中,sendfile() 这种机制的实现已经没有了,但是这个 API 以及相应的功能还存在,
只不过 API 以及相应的功能是利用了 splice() 这种机制来实现的。
可以参考下面这个链接:
http://www.ibm.com/developerworks/cn/linux/l-cn-zerocopy2/

下面这个是splice系统调用的声明和参数的含义

1
2
3
4
5
#define _GNU_SOURCE
#include <fcntl.h>

long splice(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags);

SPLICE_F_MOVE 如果合适的话,按照整页内存移动数据
SPLICE_F_NONBLOCK 非阻塞的splice操作
SPLICE_F_MORE 给内核一个提示,后续的splice系统调用将读取更多的数据
SPLICE_F_GIFT 对splice没有效果

下面是一个使用splice系统调用实现的文件的拷贝,因为splice中必须有一个管道描述符
所以需要创建一个管道,将in.txt的内容读入到管道中,然后再从管道读出到out.txt中
但这不是一个好的例子,因为管道是有容量大小的,所以到文件比较大的时候这个程序会导致复制的内容不全
需要通过fcntl来设置管道的容量,fcntl(fd,F_SETPIPE_SZ,管道容量大小);

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
35
36
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
char buf[BUFSIZ];
int n=0;
int p[2];
const char *filename1 = "in.txt";
const char *filename2 = "out.txt";
int ret = pipe(p);
if( ret == -1){
perror("create pipe failue");
exit(-1);
}
int fd = open(filename1,O_RDWR);
int fd2 = open(filename2,O_RDWR);
struct stat file_buf;
fstat(fd,&file_buf);

ret = splice(fd,NULL,p[1],NULL,file_buf.st_size,SPLICE_F_MORE|SPLICE_F_MOVE);
if( ret == -1){
perror("use splice failue");
exit(-1);
}

ret = splice(p[0],NULL,fd2,NULL,file_buf.st_size,SPLICE_F_MORE|SPLICE_F_MOVE);
if( ret == -1){
perror("use splice failue");
exit(-1);
}
}

tee系统调用

在两个管道文件描述符之间拷贝数据,也是零拷贝操作,它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。
下面是tee系统调用的函数声明

1
2
3
4
5
#define _GNU_SOURCE
#include <fcntl.h>

long tee(int fd_in, int fd_out, size_t len, unsigned int flags);
其中flags和splice系统调用一样,详细的请参考man手册

下面的例子是利用tee实现linux下tee命令
注:tee命令可以将标准输出的数据重定向到标准输出,并且同时输出到文件

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>


int main(int argc,char *argv[])
{
if(argc != 2)
{
printf("usage: %s<file>\n",argv[0]);
return 1;
}
//打开文件
int filefd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,0666);
assert(filefd > 0);

//创建管道,用于输出到标准输出
int pipefd_stdout[2];
int ret = pipe(pipefd_stdout);
assert(ret != -1);

//再创建一个管道,用于输出到文件
int pipefd_file[2];
ret = pipe(pipefd_file);
assert(ret != -1);

//将标准输出内容 输入管道
ret = splice(STDIN_FILENO,NULL,pipefd_stdout[1],NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
assert(ret != -1);

//将管道pipefd_stdout的输出赋值到文件管道
ret = tee(pipefd_stdout[0],pipefd_file[1],32768,SPLICE_F_NONBLOCK);
assert(ret != -1);

//将文件管道中的内容输出到文件
ret = splice(pipefd_file[0],NULL,filefd,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
assert(ret != -1);

close(filefd);
close(pipefd_stdout[0]);
close(pipefd_stdout[1]);
close(pipefd_file[0]);
close(pipefd_file[1]);
return 0;
}

注: 文章写的不太好,本来准备定位是原理+实践,可惜原理自己没办法说透,实践的话代码量太大,放在博客里面不太好,所以最终改成了应用,如何去使用这些系统调用

参考连接