Linux系统编程3:高级文件i/o

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;//跳出接收循环
    }
}

  • 12
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值