高级I/O函数

本文深入探讨了在套接字I/O操作中设置超时的三种主要方法,包括使用alarm信号、select函数及SO_RCVTIMEO/SO_SNDTIMEO套接字选项。此外,还讲解了recv、send、readv、writev、recvmsg和sendmsg等函数的使用,以及如何使用标准I/O函数库进行套接字操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

  1. 首先介绍了I/O操作上设置超时共有三种办法;
  2. 然后是read和write这两个函数的三个变体:
    1. recv和send
    2. readv和writev
    3. recvmsg和sendmsg
  3. 如何确定套接字接收缓冲区中的数据量;
  4. 如何在套接字上使用C的标准I/O函数库;

 

套接字超时

在设计套接字的I/O操作上设置超时的方法有以下3种:

  1. 调用alarm,它在指定超时期满时产生SIGALRM信号。这个方法涉及信号处理,而信号处理在不同的实现上存在差异,而且可能干扰进程中现有的alarm调用;
  2. 在select中阻塞等待I/O(select有内置的时间限制)。以此代替直接阻塞在read或write调用上;
  3. 使用较新的SO_RCVTIMEO和SO_SNDTIMEO套接字选项。这个方法的问题在于并非所有实现都支持这两个套接字选项;

但仍然期望可用于connect的技术,因为TCP内置的connect超时相当长(典型值为75秒)。select可用来在connect上设置超时的先决条件是相应套接字处于非阻塞模式,而那两个套接字选项对connect并不适用。

 

  • 使用SIGALRM为connect设置超时

例子:connect_timeo函数

#include "unp.h"
static void connect_alarm(int);

int connect_timeo(int sockfd, const SA *saptr, socklen_t salen, int nsec){
    Sigfunc *sigfunc;
    int n;
    sigfunc = signal(SIGALRM, connect_alarm);
    if(alarm(nsec) != 0)
        err_msg("connect_timeo: alarm was already set");

    if((n = connect(sockfd,saptr, salen)) < 0){
        close(sockfd);
        if(errno == EINTR)
            errno = ETIMEDOUT;
    }

    alarm(0);
    signal(SIGALRM, sigfunc);

    return n;
}

static void connect_alarm(int signo)
    return ;
  • 使用select为recvfrom设置超时

#include "unp.h"

int readable_timeo(int fd, int sec){
    fd_set rset;
    struct timeval tv;
    
    FD_ZERO(&rset);
    FD_SET(fd, &rset);

    tv.tv_sec = sec;
    tv.tv_usec = 0;

    return select(fd+1, &rset, NULL, NULL, &tv);
}
  • 使用SO_RCVTIMEO套接字选项为recvfrom设置超时

本选项一旦设置到某个描述符(包括指定超时值),其超时甚至将应用于该描述符上的所有读操作。本方法的优势就体现在一次性设置选项上,而前两个方法总是要求我们在欲设置时间限制的每个操作发生之前做些工作。本套接字选项仅仅应用于读操作,类似的SO_SNDTIMEO选项则仅仅应用于写操作,两者都不能用于为connect设置超时。

#include "unp.h"

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_T servlen){
    int n;
    char sendline[MAXLINE], recvline[MAXLINE + 1];
    struct timeval tv;
    
    tv.tv_sec = 5;
    tv.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
 
    while(fgets(sendline, MAXLINE, fp) != NULL){
        sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
        n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
        if(n<0){
            if(errno == EWOULDBBLOCK){
                fprintf(stderr, "socket timeout\n");
                continue;
            }else
                err_sys("recvfrom error");
        }

        recvline[n] = 0;
        fputs(recvline, stdout);
    }
}
  • recv和send函数

#include <sys/socket.h>

ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);

ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);

//返回:若成功则为读入或写出的字节数,若出错则为-1

recv和send的前3个参数等同于read和write的3个参数。flags参数的值或为0,或为下图所列的一个或多个常值的逻辑或:

  • MSG_DONTROUTE:告知内核目的主机在某个直接连接的本地网络上,因而无需执行路由表查找;
  • MSG_DONTWAIT:无需打开相应套接字的非阻塞标志前提下,把单个I/O操作临时指定为非阻塞,接着执行I/O操作,然后关闭非阻塞标志;
  • MSG_OOB:对于send,本标志指明即将发送带外数据;对于recv,本标志指明即将读入的是带外数据而不是普通数据;
  • MSG_PEER:本标志适用于recv和recvfrom,它允许我们查看已可读取的数据,而且系统不在recv或recvfrom返回后丢弃这些数据;
  • MSG_WAITALL:告知内核不要在尚未读入请求数目的字节之前让一个读操作返回。如果系统支持本标志,就可以省略掉readn函数,而替之以如下的宏:
    #define readn(fd, ptr, n) recv(fd, ptr, n, MSG_WAITALL)

     

  • readv和writev函数

这两个函数类似于read和write,不过readv和writev允许单个系统调用读入到或写出自一个或多个缓冲区。这些操作分别称为分散读和集中写,因为来自读操作的输入数据被分散到多个应用缓冲区中,而来自多个应用缓冲区的输出数据则被集中提供给单个写操作。

#include <sys/uio.h>

ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);

ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);

//返回:若成功则为读入或写出的字节数,若出错则为-1

iovec结构在头文件<sys/uio.h>中定义:

struct iovec{
    void *iov_base;    //buff的起始地址
    size_t iov_len;    //buff的长度
}
  • recvmsg和sendmsg函数

#include <sys/socket.h>

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);

//返回:若成功则为读入或写出的字节数,若出错则为-1

这两个函数把大部分参数封装到一个msghdr结构中:

struct msghdr{
    void         *msg_name;    //协议地址
    socklen_t    msg_namelen;  //协议地址的长度
    struct iovec *msg_iov;     
    int          msg_iovlen;
    void         *msg_control;
    socklen_t    msg_controllen;
    int          msg_flags;
};

【参数解析】

  1. mag_name和msg_namelen这两个成员用于套接字未连接场合;msg_name指向一个套接字地址结构,调用者在其中存放接收者(对于sendmsg调用)或发送者(对于recvmsg调用)的协议地址。如果无需指明协议地址(对于TCP套接字或已连接UDP套接字),msg_name应置为空指针;
  2. msg_iov和msg_iovlen这两个成员指定输入或输出缓冲区数组(即iovec结构数组);
  3. msg_control(cmsghdr结构)和msg_controllen指定可选的辅助数据的位置和大小;

辅助数据由一个或多个辅助数据对象构成,每个对象以一个定义在头文件<sys/socket.h>中的cmsghdr结构开头:

struct cmsghdr{
    socklen_t    cmsg_len;    //结构的长度
    int          cmsg_level;  //原始协议
    int          cmsg_type;   //协议类型
};
  • 排队的数据量

有时候我们想要在不真正读取数据的前提下知道一个套接字上已有多少数据排队等着读取。有3个技术可用于获悉已排队的数据量:

  1. 如果获悉已排队数据量的目的在于避免读操作阻塞在内核中(因为没有数据可读时我们还有其他事情可以做),那么使用非阻塞式I/O;
  2. 如果既想查看数据,又想数据仍然留在接收队列中以供本进程其他部分稍后读取,那么可以使用MSG_PEEK标志。如果我们想这样做,然而不能肯定是否真有数据可读,那么可以结合非阻塞套接字使用该标志,也可以组合使用MSG_DONTWAIT标志和MSG_PEEK标志;
  3. 一些实现支持ioctl的FIONREAD命令。该命令的第三个ioctl参数是指向某个整数的一个指针,内核通过该整数返回的值就是套接字接收队列的当前字节数,该值是已排队字节的总和;
  • 套接字和标准I/O

执行I.O的另一个方法是使用不标准I/O函数库。这个函数库由ANSI C比保准规范,意在便于移植到支持ANSI C的非Unix系统上。标准I/O函数库处理我们直接使用Unix I/O函数时必须考虑的一些细节。譬如自动缓冲输入流和输出流。

标准I/O函数库可用于套接字,不过需要考虑如下几点:

  1. 通过调用fdopen,可以从任何一个描述符创建出一个标准I/O流。类似地,通过调用fileno,可以获取一个给定标准I/O流对应的描述符;
  2. TCP和UDP套接字是全双工的。标准I/O流也可以是全双工的:只要以r+类型打开流即可,r+意味着读写。然而在这样的流上,我们必须在调用一个输出函数后插入一个fflush、fseek、fsetops或rewind调用才能接着调用一个输入函数。类似地,调用一个输入函数后也必须插入一个fseek、fsetops或rewind调用才能调用一个输出函数,除非输入函数遇到一个EOF。若使用lseek在套接字上只会失败;
  3. 解决以上问题的办法是一个给定套接字打开两个标准I/O流:一个用于读,一个用于写;

例子:使用标准I/O的str_echo函数

使用标准I/O代替read和write重新编写TCP回射程序服务器程序

#include "unp.h"

void str_echo(int sockfd){
    char line[MAXLINE];
    FILE *fpin, *fout;
    
    fpin = fdopen(sockfd, "r");
    fout = fdopen(sockfd, "w");

    while(fgets(line, MAXLINE, fpin) != NULL)
        fputs(line, fout);
}

调用fdopen创建两个标准I/O流,一个用于输入,一个用于输出。把原来的readwrite调用替换成fgetsfputs调用。

发生这个问题的原因在于:

服务器的标准I/O流被标准I/O函数库完全缓冲。这意味着该函数库把回射行复制到输出流的标准I/O缓冲区,但是不把该缓冲区中的内容写到描述符,因为该缓冲区未满。

 

标准I/O函数库执行以下三类缓冲:

  1. 完全缓冲意味着只在出现下列情况时才发生I/O:
    1. 缓冲区满;
    2. 进程显示调用fflush;
    3. 进程调用exit终止自身;
    4. 标准I/O缓冲区的通常大小为8192字节;
  2. 行缓冲意味着只在出现下列情况时才发生I/O:
    1. 碰到一个换行符;
    2. 进程调用fflush;
    3. 进程调用exit终止自身;
  3. 不缓冲意味着每次调用标准I/O输出函数都发生I/O;

标准I/O函数库的大多数Unix实现使用如下规则:

  1. 标准错误输出总是不缓冲;
  2. 标准输入和标准输出完全缓冲,除非它们指代终端设备(这种情况下它们行缓冲);
  3. 所有其他I/O流都是完全缓冲,除非它们指代终端设备(这种情况下它们行缓冲);

既然套接字不是终端设备,那么str_echo函数的上述问题就在于输出流是完全缓冲的。本问题有两个解决办法:

  1. 通过调用setvbuf迫使这个输出流变为行缓冲;
  2. 每次fputs之后通过调用fflush强制输出每个回射行;
  3. 最好的办法还是彻底避免在套接字上使用标准I/O函数库;

 

  • 高级轮训技术

/dev/poll接口

select和poll存在的一个问题是。每次调用它们都得传递待查询的文件描述符。轮训设备能在调用之间维持状态,因此轮训进程可以预先设置好待查询描述符的列表,然后进入一个循环等待事件发生,每次循环回来时不必再次设置该列表。

打开/dev/poll之后,轮训进程必须先初始化一个pollfd结构(即poll函数使用的结构,不过本机制不使用其中的revents成员)数组,再调用write往/dev/poll设备上写这个结构数组以把它传递给内核,然后执行ioctl的DP_POLL命令阻塞自身以等待事件发生。传递给ioctl调用的结构如下:

struct dvpoll{
    struct pollfd *dp_fds;
    int           dp_nfds;
    int           dp_timeout;
};

【参数解析】

  1. dp_fps:指向一个缓冲区,供ioctl在返回时存放一个pollfd结构数组;
  2. dp_nfds:指定该缓冲区的大小;
  3. dp_timeout:若无指定毫秒数则ioctl调用将一直阻塞到任何一个被轮询描述符上发生所关心的事件;
    1. 0为立即返回;
    2. -1则为没有超时设置;
#include "unp.h"
#include <sys/devpoll.h>

void str_cli(FILE *fp, int sockfd){
    int            stdineof;
    char           buf[MAXLINE];
    int            n;
    int            wfd;
    struct pollfd  pollfd[2];
    struct devpoll dopoll;
    int            i;
    int            result;

    wfd = open("/dev/poll", O_RDWR, 0);

    pollfd[0].fd = fileno(fp);
    pollfd[0].events = POLLIN;
    pollfd[0].revents = 0;

    pollfd[1].fd = sockfd;
    pollfd[1].events = POLLIN;
    pollfd[1].revents = 0;

    write(wfd, pollfd, sizeof(struct pollfd) * 2);

    stdineof = 0;
    for( ; ; ){
        dopoll.dp_timeout = -1;
        dopoll.dp_nfds = 2;
        dopoll.dp_fds = pollfd;
        result = ioctl(wfd, DP_POLL, &dopoll);

        for(i=0;i<result;i++){
            if(dopoll.dp_fds[i].fd == sockfd){
                if((n = read(sockfd, buf, MAXLINE)) == 0){
                    if(stdineof == 1)
                        return ;
                    else
                        err_quit("str_cli: server terminated prematurely");
                }
                write(fileno(stdout), buf, n);
            }else{
                //输入可读
                if((n = read(fileno(fp), buf, MAXLINE)) == 0){
                    stdineof = 1;
                    shutdown(sockfd, SHUT_WR);    //发送FIN
                    continue;
                }
            }
        }
    }
}

kqueue接口

FreeBSD引入了kqueue接口。本接口允许进程向内核注册描述所关注kqueue事件的事件过滤器。事件除了与select所关注的文件I/O和超时外,还有异步I/O、文件修改通知、进程跟踪(例如进程调用exit或fork时发出的通知)和信号处理。

#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>

int kqueue(void);
//用于注册所关注的时间,也用于确定是否有所关注时间发生
int kevent(int kq, const struct kevent *changelist, int nchanges
            struct kevent *eventlist, int nevents,
            const struct timespec *timeout);

void EV_SET(struct kevent *kev, uintptr_t ident, short filter,
            u_short flags, u_int fflags, intptr_t data, void *udata);

kevent的参数解析:

  1. changlist、nchanges:给出对所关注事件作出的更改,若无更改则分为取值NULL和0,若nchanges不为0,kevent函数就执行changelist数组中所请求的每个事件过滤器更改;
  2. eventlist:返回其条件已经出发的任何事件(包括刚在changelist中增设的那些事件);
  3. nevents:nevents个元素构成的kevent结构数组;

kevent结构在头文件<sys/event.h>中定义:

struct kevent{
    uintptr_t        ident;    //identifier
    short            filter;   //filter type
    u_short          flags;    //action flags
    u_int            fflags;   //filter-specific flags
    intptr_t         data;     //filter-specific data
    void             *udata;   //opaque user data
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值