1.分散/聚集I/O
1).readv()和writev()
#include <sys/uio.h>
ssize_t readv(int fd,const struct iovec* iov,int count);
此函数从文件描述符读取count个段(1个段是一个iovec结构体),到参数iov所指定的缓冲区中
#include <sys/uio.h>
ssize_t writev(int fd,const struct iovec* iov,int count);
此函数从参数iov指定的缓冲区中读取count个段的数据 写入fd中。
上述两个函数除了同时管控多个缓冲区以外,其他和read(),write()无差别,返回值就是实际读到/写入的字节数。第二个参数是iovec类数组。
iovec结构体是独立的,其不连接缓冲区
#include <sys/uio.h>
struct iovec{
void* iov_base;//类似于数组,每个“数组”中 可以储存iov_len”个字节长度
size_t iov_len;
};
如下例子:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/uio.h>
#include <assert.h>
#include <sys/unistd.h>
int main(){
struct iovec iov[3];
ssize_t nr;
int fd;
char *buf[]={
"hello everybody.\n",
"i an your father.\n",
"no i am just jocking.\n"
};
fd =open("tee.txt",O_CREAT|O_TRUNC|O_WRONLY);
assert(fd>=0);
for(int i=0;i<0;i++){
iov[i].iov_base=buf[i];
iov[i].iov_len=strlen(buf[i])+1;
}
nr=writev(fd,iov,3);
std::cout<<nr<<std::endl;
close(fd);
return 0;
}
如上调用WRITEV函数时,先是创建iov类的结构体数组,然后给每个数组成员指定缓存区和缓存区的字节数,就相当于是一种缓存映射,然后调用writev()函数就全部写入文件描述符了。
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/uio.h>
#include <assert.h>
int main(){
struct iovec iov[3];
ssize_t nr;
int fd;
char a1[50],a2[50],a3[50];
fd=open(fd,O_RDONLY);
iov[0].iov_base=a1;
iov[0].iov_len=sizeof(a1);
iov[1].iov_base=a2;
iov[1].iov_len=sizeof(a2);
iov[2].iov_base=a3;
iov[2].iov_len=sizeof(a3);
nr=readv(fd,iov,3);
for(int i=0;i<3;i++){
std::cout<<(char*)iov[i].iov_base<<std::endl;
}
close(fd);
}
调用readv()函数同理,要先指明iovec结构体中iov_base与外界缓存的映射和字节长度,然后在调用readv()函数,将分散的数据集中起来。然后数据储存在每一个iovec[i].iov_base中。
2.epoll(用法见网络编程笔记部分)
1).int epoll_create(int);
此函数创造EPOLL池(实例),传入参数是请求以该数作为文件描述符的整数,但一般操作系统不会,而是随机生成一个可用的EPOLL实例文件描述符。
2)epoll_ctl(int epollfd,int op,int fd,struct epoll_event* eve)可以进行EPOLL池中改变监听的文件描述符.
参数op控制行为,有常用的如下:
EPOLL_CTL_ADD:加入fd进EPOLL池中.
EPOLL_CTL_DEL:将指定文件描述符从EPOLL池中删除。
EPOLL_CLT_MOD:改变指定文件描述符的监听事件。
epoll_event里的监听事件,举例常用三种:
EPOLLET:开启边缘触发模式。
EPOLLIN:文件未阻塞,可读
EPOLLOUT:文件未阻塞,可写。
EPOLLONESHOT:
- 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
- 我们期望的是一个socket连接在任一时刻都只被一个线程处理并且同时若监听多个事件,我们也希望最多触发一个事件,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件.注意,若未重置,其不在监听,但是文件描述符还在EPOLL池里面,没被删除。
EPOLLRDHUP:此事件是检测文件描述符关闭的,如果正常关闭,则会触发该事件(准确是EPOLLIN + EPOLLRDHUP),我们关闭对应文件描述符即可(具体如何触发,是只触发一次还是一直触发是看ET还是LT)。 但有些系统检测不到,可以使用recv/read返回0,删除掉事件,关闭close(fd),EPOLLRDHUP/EPOLLHUP事件放在监听最前面。
EPOLLHUP:表示读写都关闭。
所以我们通过epoll监听对应文件描述符发生的事件可以去选择是否不管,不在局限于根据recv()/read()函数返回值来判断。
3).
int epoll_wait(int epollfd,struct epoll_event* events,int max,int timeout);
第二个是epoll_event类的数组,其用于将所有发生监听的事件进行保留,集中在一起储存在events中
第三个参数是监听的最大事件数量。
第四个参数是超时时长。
返回的是实际监听到的事件个数。
4).边缘触发和条件触发:
对于边缘触发,其探测变化是“有数据”,这个变化状态会触发通知,而且事件仅仅会触发一次,因为只会触发一次,所以只要触发就必须进行完成操作。边缘触发的条件比条件触发的条件更符合实际。由于边缘触发是“有数据”,所以通常是设置为非阻塞I/O。
下面是epoll非阻塞io和EPOLLONESHOT组成的简单服务器代码:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024
struct fds
{
int epollfd;
int sockfd;
};
int setnonblocking( int fd )
{
int old_option = fcntl( fd, F_GETFL );
int new_option = old_option | O_NONBLOCK;
fcntl( fd, F_SETFL, new_option );
return old_option;
}
void addfd( int epollfd, int fd, bool oneshot )
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;//同时注册ET和LT,但最多只触发一个
if( oneshot )
{
event.events |= EPOLLONESHOT;
}
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
setnonblocking( fd );
}
void reset_oneshot( int epollfd, int fd )
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;//发生事件后,需要重新注册
epoll_ctl( epollfd, EPOLL_CTL_MOD, fd, &event );
}
void* worker( void* arg )
{
int sockfd = ( (fds*)arg )->sockfd;
int epollfd = ( (fds*)arg )->epollfd;
printf( "start new thread to receive data on fd: %d\n", sockfd );
char buf[ BUFFER_SIZE ];
memset( buf, '\0', BUFFER_SIZE );
while( 1 )
{
int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
if( ret == 0 )
{
close( sockfd );
printf( "foreiner closed the connection\n" );
break;//对面关闭,直接退出即可
}
else if( ret < 0 )/* 即使对面发送完信息,因为是while()循环,他会继续接受,但是由于设置了非阻塞,就是对面没有在法消息过来了,所以这是recv返回-1*/
{/*并设置为EAGAIN,直接重置套接字监听退出即可,以便监听下一次事件来*/
if( errno == EAGAIN )
{
reset_oneshot( epollfd, sockfd );
printf( "read later\n" );
break;
}else{
break;//其他错误,不用重置,直接退出。
}
else
{
printf( "get content: %s\n", buf );
sleep( 5 );
}
}
printf( "end thread receiving data on fd: %d\n", sockfd );
}
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
int ret = 0;
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( listenfd >= 0 );
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( listenfd, 5 );
assert( ret != -1 );
epoll_event events[ MAX_EVENT_NUMBER ];
int epollfd = epoll_create( 5 );
assert( epollfd != -1 );
addfd( epollfd, listenfd, false );
while( 1 )
{
int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
if ( ret < 0 )
{
printf( "epoll failure\n" );
break;
}
for ( int i = 0; i < ret; i++ )
{
int sockfd = events[i].data.fd;
if ( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
addfd( epollfd, connfd, true );//都注册非阻塞和监听一次就不在监听的事件。
}
else if ( events[i].events & EPOLLIN )
{
pthread_t thread;
fds fds_for_new_worker;
fds_for_new_worker.epollfd = epollfd;
fds_for_new_worker.sockfd = sockfd;/*赋值操作不会增加文件描述符的引用计数,只有fork()函数创造子进程的时候会*/
pthread_create( &thread, NULL, worker, ( void* )&fds_for_new_worker );
}
else
{
printf( "something else happened \n" );
}
}
}
close( listenfd );
return 0;
}
注意一点的是sockfd是不能主次一次性监听的,因为要循环接受对面的请求。
3.内存映射:
此功能由内核提供,支持将文件映射到内存当中,内存地址和文件数据一一对应。
1).mmap()函数
#include <sys/mmap.h>
void* mmap(void* addr,size_t len,int port,int flags,int fd,off_t offset);
此函数将文件描述符fd指向的对象的len字节数据映射到内存中,起始位置从offset开始。
若指定addr,则优先使用addr作为起始地址,flags指定其他操作模式。port指定访存权限。
因为返回的是指针,所以可以用指针操作代替文件描述符的操作
addr告诉内核映射文件的最佳地址,但仅仅是建议,大部分addr填零,成功调用时返回的地址才是真实开始地址,调用失败则会返回MAP_FAILED并设置errno。
port参数如下:(可以按位进行或运算)
PORT_READ:页可读。
PORT_WRITE 页可写。
PORT_EXEC:页可执行
注意:PORT参数设置的访问权限不能和打开文件的模式冲突,比如某文件以只读的方式打开,PORT参数就不能设置为PORT_WRITE.不然会出无效参数错误
flags参数列举常用:
MAP_PRIVATE:表示映射区不共享,文件映射采用写时复制且写后的内容不会回写物理磁盘,不会反应在文件上,文件内容不变。在此模式下,只需要open()文件时,有读权限即可,mmap()映射权限不受限制,可读可写,因为不会回写硬盘,只写内存/
MAP_SHARED:表示和所以其他映射该文件的进程共享内存。对内存的写等效于对文件的写,且收到其他进程的影响。使用后会回写到到物理磁盘上。
上述两个操作选项必须选择一个,不能按位进行或运算。且当映射文件时,文件描述符的引用计数进行+1,取消映射或者终止当前进程,引用计数减1.
如上:mmap()直接联立内核地址与文件地址的映射。即等效于直接从内核中进行操作(比如读操作,文件对应内容写到内核中或者直接在内核页缓存中寻找,内核不用拷贝到用户缓冲区)
void* p;
p=mmap(0,len,PORT_READ,MAP_SHARED,fd,0);
这就是以只读映射到fd指向的文件,从第一个字节开始,长度为len
再补充:对于mmap()而言,其操作的操作单元是页,addr和offset参数都必须要按页对齐,映射区域为页的整数倍。如果开头未对齐页头,则在映射的最后一个有效字节的位置,会用0填充其所在的最后页区域,保证是整数倍页的大小。
可以用函数sysconf()来进行查看页的大小。
include <unsitd.h>
long page_size=sysconf(_SC_PAGESIZE);
指定宏_SC_PAGESIZE,调用失败返回-1.
2).munmap():此函数的作用就是用来取消mmap()创建的映射。
include <sys/mmap.h>
int munmap(void* addr,size_t len);
addr:mmap()返回的地址.
len:调用mmap()指定映射的大小。
调用成功返回0,调用失败返回唯一errno=EINVAL。
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
int main(){
int fd,n;
char mg[]="it is a test]n";
char ch;
fd=open("testmmap",O_RDWR|O_CREAT|O_TRUNC,0644);//可读可写打开
if(fd==-1){
perror("open error");
}
/*
lseek(fd,10,SEEK_END);//文件指针指向零并开始扩容
write(fd,"\0",1);
等价下面的ftruncate()函数
*/
ftruncate(fd,10);//自动添加\0 最多写9个字符
int len=lseek(fd,0,SEEK_END);
void* p=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//可读可写映射
if(p==MAP_FAILED){
perror(" mmap error");
}
strcpy((char *)p,"hello,mmap");//写操作
std::cout<<(char*)p<<std::endl;//读操作
int ret=munmap(p,len);
if(ret==-1){
perror("munmap error");
}
return 0;
}
fstat函数用于查看文件的指定信息。用stat结构体。mmap()函数用读的方式进行映射.然后从对应文件位置开始文件读(标准输出)。
映射后可以减少拷贝,这个注意,从文件中读取到指定BUFF中是拷贝,不是从文件中转移。
3).msync()函数:其作用和fsync()类似。其可以将mmap()函数生成的映射在内存中的任何修改都写回磁盘中,实现同步映射。(文件在内存中的映射从addr开始的len长度字节被写回到磁盘中)
#include <sys/mmap.h>
int msync(void* addr,size_t len,int flags);
前两个参数一般是调用mmap()函数一样的。
不调用同步映射,无法保证修改过的映射会回写到硬盘中。
当内存映射写数据时,进程会直接修改内核页缓存中的文件页(本质就是修改文件),无需经过
内核,所以内核不会立即同步页缓存到硬盘。
而write()函数是写入用户缓存区后加入消息队列等待被回写入磁盘。
FLAGS:
MS_SYNC:指定同步操作必须同步进行,直到所有页写回磁盘,才会返回。
MS_ASYNC:指定同步操作应该是异步执行,更新由操作系统调用,而函数会立刻返回。
msync()函数中必须指定其中一个参数,不能二者公用。调用失败返回-1.
至于文件映射从addr~addr+len进行怎样的操作,可以用函数进行madvise()进行调整。自行查阅。
MMAP()使用注意事项:
1.用于创建映射区文件大小为0(比如截断,文件页大小为0),实际MMAP函数中指定非零大小创建映射区,会出总线错误。
2.用于创建映射区文件大小为0,实际MMAP函数中指定0大小创建映射区,会出无效参数错误。
3.创建映射区,在以MAP_SHARED情况下,至少需要读权限,MMAP()的读写权限需要<=文件的open()权限,文件至少要有一个读权限,只写不行。注意和MAP_PRIVATE的情况进行区别。
4.文件描述符fd,和shm_open()一样,在MMAP创建完映射区后即可关闭,后续访问文件,用地址访问。
5.offset只能是4K,4096的整数倍,因为分页。
6.对于MMAP()申请的内存,不要进行越界访问。
7.mummap()函数使用的指针必须是mmap()调用返回的指针,不能进行改变。
MMAP()可以用于父子进程之间的内容共享通信(pipe只能有血缘关系进行通信且单向流动,socketpair()创建的双全工通道不仅父子,还可以子进程和子进程通信,这点很重要。)
如下面例子:
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
int var=100;
int main(){
int fd,n;
fd=open("testmmap",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd==-1){
perror("open error");
}
ftruncate(fd,4);//开辟内容 最多写3个字符
void* p=mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//映射区共享
/*如果是MAP_PRIVATE 这是父进程私有的内存,fork()后 子进程的p还是指向私有内存,但是是
写时复制,子进程修改只能在子进程中看见,父进程修改只能在父进程看见,等同于var*/
if(p==MAP_FAILED){
perror(" mmap error");
}
close(fd);
pid_t pid=fork();
if(pid==0){
*(int*)p=2000;//写操作
var=1000;
std::cout<<var<<" "<<*((int*)p)<<std::endl;
}else{
sleep(1);
std::cout<<"father :"<<*(int*)p<<" "<<var<<std::endl;
int ret=munmap(p,4);
}
return 0;
}
子进程往里面写入2000,因为映射是共享内存,所以父进程能读到子进程写的2000,但对于var,父子进程写时复制.
对于非血缘关系的进程(地位同等的进程,不是父子关系):mmap:数据可以重复读取,比如一个进程负责往里面写数据,一个进程负责读共享内存的数据。
LINUX知识补充:
预读:
当请求加载文件的某块内容时,内核也会读取被加载块的下一块,如果随后请求访问下一块,内核可以马上返回上面数据。预读有性能提高,提高和额外开销依赖于预读窗口大小,内核会动态调整预读窗口,保证预读的命中率。
异步和同步I/O:
用户空间I/O调度------>利于寻址操作,用户空间会运用一些算法进行所有的寻址处理。(不考虑内核,更多在操作系统书中):
按照绝对路径排序,按照inode排序,按照请求文件对应的物理块进行排序。
补充recv()函数,前文已经提及,read(),write()函数无法用于设置超时。
而recv()函数可以,所以用recv()函数代替read()函数最好,其还能设置超时设置(setsockopt函数)。
注意read(),write()函数对于操作的文件描述符设置了O_NONBLOCK,则会变为非阻塞I/O。
#include <unistd.h>
#include <sys/stat.h>
int recv(int fd,char* buf,int len,int falgs);
阻塞与非阻塞recv返回值没有区分,都是
> 0 成功接收数据大小。
= 0 另外一端关闭了套接字
= -1 错误,需要获取错误码errno
返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)
的情况下认为连接是正常的,继续接收。
只是阻塞模式下recv会阻塞着接收数据,非阻塞模式下如果没有数据会返回,
不会阻塞着读,因此需要循环读取)。
while(1)
{
cnt = (int)recv(m_socket, pBuf,RECVSIZE, 0);
if( cnt >0 )
{
//正常处理数据
}
else
{
if((cnt<0) &&(errno == EAGAIN||errno == EWOULDBLOCK||errno == EINTR))
//这几种错误码,认为连接是正常的,继续接收,或者可以断开,更细节的区分方式,见前文EL/LT的代码
{
continue;//继续接收数据
}
break;//跳出接收循环
}
}