跳转至

UNIX io

1 dup和dup2

  • 功能:复制文件描述符
  • 语法:
  • int dup (int __fd):复制fd文件描述符信息,并返回一个新的文件描述符(当前最小可用文件描述符号)
  • int dup2 (int __fd, int __fd2):复制fd描述符到fd2,如果fd2已打开就先关闭再复制
  • 区别:dup2可以指定被复制的描述符号,而dup由系统分配
  • 示例:dup2
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
int main()
{
    int fd_new=100;
    int fd=open("file1",O_RDWR);
    printf("fd=%d\n",fd);
    printf("ret=%d",dup2(fd,fd_new));
    printf("fd_new=%d\n",fd_new);
    write(fd_new,"hello",5);
    close(fd_new);
}
//输出:
//fd=3
//ret=100fd_new=100
  • 示例:dup
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
int main()
{
    int fd=open("file1",O_RDWR);
    printf("fd=%d\n",fd);
    int fd_new=dup(fd);
    printf("fd_new=%d\n",fd_new);
    write(fd_new,"hello",5);    
}
//输出:
//fd=3
//fd_new=4

2 fcntl

  • 功能:用来操作文件描述符的一些特性(比如改变open时设置的flag信息)
  • 语法:int fcntl (int __fd, int __cmd, ...);
  • fd:文件描述符
  • cmd:文件描述符要操作的指令
    • F_DUPFD:用来查找大于或等于参数arg的最小且仍未使用的文件描述词,并且复制参数fd的文件描述词。执行成功则返回新复制的文件描述词
    • F_GETFD:获取close-on-exec旗标。若此旗标的FD_CLOEXEC位为0,代表在调用exec()相关函数时文件将不会关闭
    • F_SETFD:设置close-on-exec旗标。该旗标以参数arg 的FD_CLOEXEC位决定
    • F_GETFL:取得文件描述词状态旗标,此旗标为open()的参数flags
    • F_SETFL:设置文件描述词状态旗标,参数arg为新旗标,但只允许O_APPEND、O_NONBLOCK和O_ASYNC位的改变,其他位的改变将不受影响
    • F_GETLK:取得文件锁定的状态
    • F_SETLK:设置文件锁定的状态(非阻塞)。此时flcok 结构的l_type 值必须是F_RDLCK、F_WRLCK或F_UNLCK。如果无法建立锁定,则返回-1
    • F_SETLKW:设置文件锁定的状态(阻塞)。与 F_SETLK 作用相同,但是无法建立锁定时,此调用会一直等到锁定动作成功为止
  • 可选参数:根据不同的参数cmd决定

2.1 设置文件状态

  • 示例:覆盖写改为追加写
#include<fcntl.h>
#include<unistd.h>
int main()
{
    char buf[1024]={0};
    int fd=open("file1",O_RDWR);
    if(fd<0)
        return 0;
    int sz=0;
    unsigned int flag=fcntl(fd,F_GETFL);
    //给文件增加追加模式,取代open时设置的覆盖写模式
    flag=fcntl(fd,F_SETFL,flag|O_APPEND);
    sz=write(fd," world",6);
    close(fd);
}

2.2 扩展:fd的close on exec

  • 参考资料
  • 功能:当父进程fork子进程时,使子进程不继承父进程中的文件描述符(默认继承),可以在open中设置也可以通过fcntl设置
  • 示例
#include<fcntl.h>
#include<unistd.h>
int main()
{
    char buf[1024]={0};
    //在open时设置FD_CLOEXEC
    int fd=open("file1",O_RDWR|FD_CLOEXEC);
    if(fd<0)
        return 0;
    int sz=0;
    unsigned int flag=fcntl(fd,F_GETFD);
    //通过fcntl设置FD_CLOEXEC
    flag=fcntl(fd,F_SETFD,FD_CLOEXEC);
    sz=write(fd," world",6);
    close(fd);
}

3 ioctl

4 io复用模式

  • select有描述符限制,目前最多有FD_SETSIZE=1024上限;pool则没有此限制
  • selectpoll采用轮询方式,效率低;epoll采用事件通知机制

4.1 select

  • 功能:用来等待文件描述词状态的改变
  • 语法:int select (int nfds, fd_set *readfds,fd_set *writefds,fd_set *exceptfds,timeval *timeout);
  • nfd:是readfdswritefds文件描述符集合中最大值+1
  • readfds:可读文件描述符集
  • writefds:可写文件描述符集
  • exceptfds:异常文件描述符集
  • timeout:设置监视的等待事件;当NULL时,表示会一直阻塞等待;当超过timeout时会返回;当timeout设置为0时,会立即返回
  • 返回值:执行成功,返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,当有错误发生时则返回-1
  • 关联接口
  • FD_SET(fd, fdsetp):向fdsetp中添加fd
  • FD_CLR(fd, fdsetp):从fdsetp移除fd
  • FD_ISSET(fd, fdsetp):测试fd是否是fdsetp中且状态改变了的(用于判断哪一个描述符状态变了)
  • FD_ZERO(fdsetp):清空fdsetp集合
  • 示例
#include<sys/select.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
    timeval tv;
    fd_set rfds;//作为读描述符集合
    FD_ZERO(&rfds);//清零描述符集合
    int fd=0;
    FD_SET(fd, &rfds);//设置监听标准输入
    tv.tv_sec = 5;//设置最大等待时长为5秒
    int ret=select(fd+1,&rfds,NULL,NULL,&tv);//等待描述符可读
    if (ret<0)
    {
        perror("select");
        return -1;
    }
    else if (ret==0)
    {
        printf("timeout\n");
    }
    else
    {
        printf("read\n");
    }
    return 0;   
}
//输出
//ret=1
//read
//write data is hello world!

4.2 poll

  • 功能:用来等待文件描述词状态的改变
  • 语法:int poll(struct pollfd fds[], nfds_t nfds, int timeout);
  • fds:文件描述符信息集合
  • nfds:fds集中最大描述符+1
  • timeout:阻塞等待时间;-1一直等待;0会立即返回;大于0表示会阻塞等待最多多少毫秒
  • 返回值:执行成功,返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,当有错误发生时则返回-1
  • pollfd结构如下
/* Data structure describing a polling request.  */
struct pollfd
  {
    int fd;         /* File descriptor to poll.  */
    short int events;       /* Types of events poller cares about.  */
    short int revents;      /* Types of events that actually occurred.  */
  };
  • 事件类型如下:
/* Event types that can be polled for.  These bits may be set in `events'
   to indicate the interesting event types; they will appear in `revents'
   to indicate the status of the file descriptor.  */
#define POLLIN      0x001       /* There is data to read.  */
#define POLLPRI     0x002       /* There is urgent data to read.  */
#define POLLOUT     0x004       /* Writing now will not block.  */

#if defined __USE_XOPEN || defined __USE_XOPEN2K8
/* These values are defined in XPG4.2.  */
# define POLLRDNORM 0x040       /* Normal data may be read.  */
# define POLLRDBAND 0x080       /* Priority data may be read.  */
# define POLLWRNORM 0x100       /* Writing now will not block.  */
# define POLLWRBAND 0x200       /* Priority data may be written.  */
#endif

#ifdef __USE_GNU
/* These are extensions for Linux.  */
# define POLLMSG    0x400
# define POLLREMOVE 0x1000
# define POLLRDHUP  0x2000
#endif

/* Event types always implicitly polled for.  These bits need not be set in
   `events', but they will appear in `revents' to indicate the status of
   the file descriptor.  */
#define POLLERR     0x008       /* Error condition.  */
#define POLLHUP     0x010       /* Hung up.  */
#define POLLNVAL    0x020       /* Invalid polling request.  */
  • 示例
#include<sys/poll.h>
#include<stdio.h>
int main()
{
    struct pollfd fds[2];
    fds[0].fd = 0;
    fds[0].events = POLLIN;
    fds[1].fd = 1;
    fds[1].events = POLLOUT;
    int ret=poll(fds, 2, -1);
    printf("ret=%d\n", ret);
    for (size_t i = 0; i < 2; i++)
    {
        if (fds[i].revents & POLLIN)
        {
            printf("i=%lu fd=%d, POLLIN\n", i,fds[i].fd);
        }
        if (fds[i].revents & POLLOUT)
        {
            printf("i=%lu fd=%d, POLLOUT\n", i,fds[i].fd);
        }
    }
    return 0;
}
//输出
//ret=1
//i=1 fd=1, POLLOUT

4.3 epoll

  • 参考 epoll库提供了3个系统函数:
  • epoll_create:创建epoll实例,返回的epoll实例需要使用close关闭
  • epoll_ctl:管理epoll实例
  • epoll_wait:等待发生在epoll实例上的事件

epoll的事件类如下:

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

struct epoll_event
{
  uint32_t events;  /* Epoll events */
  epoll_data_t data;    /* User data variable */
} __EPOLL_PACKED;

4.3.1 LT、ET 模式

  • LT (Level Trigger):水平触发,一个事件只要有,就会一直触发。(默认)
  • ET (Edge Trigger):边缘触发,只有一个事件从无到有才会触发。(设置 ev.events=EPOLLET)
title: socker读写事件理解LT、ET模式
- 以 socket 的读事件为例,对于水平模式,只要 socket 上有未读完的数据,就会一直产生 POLLIN 事件;
- 而对于边缘模式,socket 上第一次有数据会触发一次,后续 socket 上存在数据也不会再触发,除非把数据读完后,再次产生数据才会继续触发。
- 对于 socket 写事件,如果 socket 的 TCP 窗口一直不饱和,会一直触发 POLLOUT 事件;
- 而对于边缘模式,只会触发一次,除非 TCP 窗口由不饱和变成饱和再一次变成不饱和,才会再次触发 POLLOUT 事件。
  • 使用 ET 模式需要注意正确的读写方式为:
    • 读:只要可读,就一直读,直到返回 0,或者 errno = EAGAIN
    • 写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN
  • 正确的读 :
n = 0;  
while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {  
    n += nread;
}  
if (nread == -1 && errno != EAGAIN) {  
    perror("read error");  
}
  • 正确的写 :
int nwrite, data_size = strlen(buf);  
n = data_size;  
while (n > 0) {  
    nwrite = write(fd, buf + data_size - n, n);  
    if (nwrite < n) {  
        if (nwrite == -1 && errno != EAGAIN) {  
            perror("write error");  
        }  
        break;  
    }  
    n -= nwrite;  
}

4.3.2 epoll_create

  • 功能:创建epoll实例
  • 语法:
  • int epoll_create(int size);
    • size:从Linux 2.6.8后,可以忽略,但必须指定一个大于0的数
  • int epoll_create1(int flags);
    • flags : 当 flags == 0 时和 epoll_create 相同;flags 还可以设置 EPOLL_CLOEXEC
  • 返回值:成功时,返回引用到epoll实例的文件描述符,这个描述符会在后续epoll操作上使用;错误时,返回-1

4.3.3 epoll_ctl

  • 功能:管理epoll实例
  • 语法:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd:epoll_create返回的epoll实例
  • op:可选3种选项
    • EPOLL_CTL_ADD 1 / Add a file descriptor to the interface. /
    • EPOLL_CTL_DEL 2 / Remove a file descriptor from the interface. /
    • EPOLL_CTL_MOD 3 / Change file descriptor epoll_event structure. /
  • fd:需要操作的文件描述符
  • event:指定事件信息
  • 返回值:成功返回0;失败返回-1

4.3.4 epoll_wait

  • 功能:等待io事件的发生
  • 语法:
  • int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
    • epfd:epoll_create返回的描述符
    • events:如果有事件到来,存储事件
    • timeout:阻塞等待时间;-1一直等待;0会立即返回;大于0表示会阻塞等待最多多少毫秒
    • 返回值:大于0,表示到来的事件数;等于0,表示没有事件发送;小于0,表示发送错误
  • int epoll_pwait(int epfd, struct epoll_event *events,int maxevents, int timeout,const sigset_t *sigmask);
  • 示例
#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

using namespace std;

#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
#define INFTIM 1000

void setnonblocking(int sock)
{
    int opts;
    opts=fcntl(sock,F_GETFL);
    if(opts<0)
    {
        perror("fcntl(sock,GETFL)");
        exit(1);
    }
    opts = opts|O_NONBLOCK;
    if(fcntl(sock,F_SETFL,opts)<0)
    {
        perror("fcntl(sock,SETFL,opts)");
        exit(1);
    }
}

int main(int argc, char* argv[])
{
    int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
    ssize_t n;
    char line[MAXLINE];
    socklen_t clilen;

    if ( 2 == argc )
    {
        if( (portnumber = atoi(argv[1])) < 0 )
        {
            fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
            return 1;
        }
    }
    else
    {
        fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
        return 1;
    }

    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件 
    struct epoll_event ev,events[20];
    //生成用于处理accept的epoll专用的文件描述符 
    epfd=epoll_create(256);
    struct sockaddr_in clientaddr;
    struct sockaddr_in serveraddr;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    //把socket设置为非阻塞方式 
    //setnonblocking(listenfd); 
    //设置与要处理的事件相关的文件描述符 
    ev.data.fd=listenfd;
    //设置要处理的事件类型 
    ev.events=EPOLLIN|EPOLLET;
    //ev.events=EPOLLIN; 
    //注册epoll事件 
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    char *local_addr="127.0.0.1";
    inet_aton(local_addr,&(serveraddr.sin_addr));//htons(portnumber); 
    serveraddr.sin_port=htons(portnumber);
    bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(listenfd, LISTENQ);
    maxi = 0;
    for ( ; ; ) {
        //等待epoll事件的发生 
        nfds=epoll_wait(epfd,events,20,500);
        //处理所发生的所有事件 
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd)//如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。 
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
                if(connfd<0){
                    perror("connfd<0");
                    exit(1);
                }
                //setnonblocking(connfd); 
                char *str = inet_ntoa(clientaddr.sin_addr);
                cout << "accapt a connection from " << str << endl;
                //设置用于读操作的文件描述符 
                ev.data.fd=connfd;
                //设置用于注测的读操作事件 
                ev.events=EPOLLIN|EPOLLET;
                //ev.events=EPOLLIN; 
                //注册ev 
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events&EPOLLIN)//如果是已经连接的用户,并且收到数据,那么进行读入。 
            {
                cout << "EPOLLIN" << endl;
                if ( (sockfd = events[i].data.fd) < 0)
                    continue;
                if ( (n = read(sockfd, line, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                        close(sockfd);
                        events[i].data.fd = -1;
                    } else
                        std::cout<<"readline error"<<std::endl;
                } else if (n == 0) {
                    close(sockfd);
                    events[i].data.fd = -1;
                }
                line[n] = '/0';
                cout << "read " << line << endl;
                //设置用于写操作的文件描述符 
                ev.data.fd=sockfd;
                //设置用于注测的写操作事件 
                ev.events=EPOLLOUT|EPOLLET;
                //修改sockfd上要处理的事件为EPOLLOUT 
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); 
            }
            else if(events[i].events&EPOLLOUT) // 如果有数据发送 
            {
                sockfd = events[i].data.fd;
                write(sockfd, line, n);
                //设置用于读操作的文件描述符 
                ev.data.fd=sockfd;
                //设置用于注测的读操作事件 
                ev.events=EPOLLIN|EPOLLET;
                //修改sockfd上要处理的事件为EPOLIN 
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            }
        }
    }
    return 0;
}

5 mmap和munmap

  • 功能:一种将文件或设备映射或解除映射到内存中的技术
  • 优点:
  • 读写文件避免了 read() 和 write() 系统调用,也避免了数据的拷贝。
  • 除了潜在的页错误,读写 map 后的文件不引起系统调用或者上下文切换。就像访问内存一样简单。
  • 多个进程 map 同一个对象,可以共享数据。
  • 可以直接使用指针来跳转到文件某个位置,不必使用 lseek() 系统调用。
  • 劣势
  • 内存浪费。由于必须要使用整数页的内存。
  • 导致难以找到连续的内存区域
  • 创建和维护映射和相关的数据结构的额外开销。在大文件和频繁访问的文件中,这个开销相比 read write 的 copy 开销小。
  • 参考资料
  • 示例
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)
int
main(int argc, char *argv[])
{
    char *addr;
    int fd;
    struct stat sb;
    off_t offset, pa_offset;
    size_t length;
    ssize_t s;
    if (argc < 3 || argc > 4) {
        fprintf(stderr, "%s file offset [length]\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    fd = open(argv[1], O_RDONLY);
    if (fd == -1)
        handle_error("open");
    if (fstat(fd, &sb) == -1)           /* To obtain file size */
        handle_error("fstat");
    offset = atoi(argv[2]);
    pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);
        /* offset for mmap() must be page aligned */
    if (offset >= sb.st_size) {
        fprintf(stderr, "offset is past end of file\n");
        exit(EXIT_FAILURE);
    }
    if (argc == 4) {
        length = atoi(argv[3]);
        if (offset + length > sb.st_size)
            length = sb.st_size - offset;
                /* Canaqt display bytes past end of file */
    } else {    /* No length arg ==> display to end of file */
        length = sb.st_size - offset;
    }
    addr = mmap(NULL, length + offset - pa_offset, PROT_READ,
                MAP_PRIVATE, fd, pa_offset);
    if (addr == MAP_FAILED)
        handle_error("mmap");
    s = write(STDOUT_FILENO, addr + offset - pa_offset, length);
    munmap(addr, length + offset - pa_offset);
    if (s != length) {
        if (s == -1)
            handle_error("write");
        fprintf(stderr, "partial write");
        exit(EXIT_FAILURE);
    }
    exit(EXIT_SUCCESS);
}

5.1 mmap

  • 语法:void *mmap (void *addr, size_t len, int prot,int flags, int fd, __off_t offset)
  • addr:一般设置NULL即可,mmap执行成功会返回一个执行原文件数据的地址
  • len:文件长度
  • prot:指定映射内存保护方式,注意不能与文件访问方式冲突,比如文件以读打开,prot则不能是PROT_WRITE
    • PROT_READ: 0x1 / Page can be read. /
    • PROT_WRITE: 0x2 / Page can be written. /
    • PROT_EXEC: 0x4 / Page can be executed. /
    • PROT_NONE: 0x0 / Page can not be accessed. /
  • flag:指定映射的类型
    • MAP_SHARED:和其他进程共享这个文件。往内存中写入相当于往文件中写入。会影响映射了这个文件的其他进程
    • MAP_PRIVATE:任何内存中的改动并不反映到文件之中,也不反映到其他映射了这个文件的进程之中。如果只需要读取某个文件而不改变文件内容,可以使用这种模式
  • fd:打开的文件描述符(mmap之后引用技术+1,因此可以close关闭)
  • offset:文件偏移长度
  • 返回值:成功,返回内存映射地址;失败返回MAP_FAILED(-1)

5.2 munmap

  • 语法:int munmap (void *addr, size_t len);
  • addr:映射的内存地址(对应mmap的返回值)
  • len:内存字节长度(对应mmap的len参数)
  • 返回值:成功返回0,失败返回-1