select 源码解析

函数原型:int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const timeval* timeout)

select()的调用path如下:sys_select -> core_sys_select -> do_select

1、sys_select()

select属于系统调用,它进入内核态之后就调用sys_select()。

功能:
检查参数“超时时间”是否有效以及对超时时间进行转化。

2、core_sys_select()

功能:
1)检查select()第一个参数,是否大于“当前进程所能打开的最大文件数量(一般为1024)”。若大于,则调整为“当前进程所能打开的最大文件数量”。

2)用结构体 fd_set_bits 保存用户传进来的参数:

typedef struct 
{
    unsigned long *in, *out, *ex;
    unsigned long *res_in, *res_out, *res_ex;
} fd_set_bits;

注:
in、out、ex分别保存用户注册的感兴趣事件(个人理解为将 fd_set* 转化为二进制位图形式),res_in、res_out、res_ex分别保存用户感兴趣的已发生事件(二进制位图形式,最终需转化为 fd_set*)。

3、do_select()

原理:
1)每次同时从 fd_set_bits.in、fd_set_bits.out、fd_set_bits.ex 取一个值,即取一个 long 字节长度数目的 socket。将三者进行“或运算”。
若运算结果不为0,则表明其中有用户感兴趣的 socket,否则对下一个 long 字节长度数目的 socket 进行判断。

2)对 long 字节的每一位进行遍历:(bit=1)

  • 调用该位所对应 socket 的驱动程序的poll函数。
  • 若 poll 函数 检测到该位上有“POLLIN_SET”事件发生,即“函数返回值 & POLLIN_SET”不为0,即可读,则进一步判断是否为 fd_set_bits.in 该 long 字节的某一位发生事件(每次遍历将bit左移一位,并与 fd_set_bits.in 的该 long 位进行“与运算”,这是因为 POLLIN_SET 仅表示3者中至少一个发生可读事件)。
  • 若 poll 函数 检测到该位上有“POLLOUT_SET”事件发生,即可写,则进一步判断是 fd_set_bits.out 该 long 字节的哪一位发生事件。
  • 若 poll 函数 检测到该位上有“POLLEX_SET”事件发生,即异常,则进一步判断是 fd_set_bits.ex 该 long 字节的哪一位发生事件。

3)若对所有 fd_set遍历后,满足条件:用户感兴趣的 socket 上有事件发生、超时或当前进程有未处理的信号,则停止继续遍历;否则,调用 schedule_timeout 进行睡眠,直至超时或因事件发生而被唤醒后,重复步骤1、2,继续对所有 fd_set 进行遍历。

4)返回值:返回发生事件的 socket 个数。

源码

int do_select(int n, fd_set_bits *fds, s64 *timeout)    
{    
 struct poll_wqueues table;    
 poll_table *wait;    
 int retval, i;    

 rcu_read_lock();    
 /*根据已经打开fd的位图检查用户打开的fd, 要求对应fd必须打开, 并且返回最大的fd*/   
 retval = max_select_fd(n, fds);    
 rcu_read_unlock();    

 if (retval < 0)    
  return retval;    
 n = retval;    


 /*将当前进程放入自已的等待队列table, 并将该等待队列加入到该测试表wait*/   
 poll_initwait(&table);    
 wait = &table.pt;    

 if (!*timeout)    
  wait = NULL;    
 retval = 0;    

 for (;;) {/*死循环*/   
  unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;    
  long __timeout;    

  /*注意:可中断的睡眠状态*/   
  set_current_state(TASK_INTERRUPTIBLE);    

  inp = fds->in; outp = fds->out; exp = fds->ex;    
  rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;    


  for (i = 0; i < n; ++rinp, ++routp, ++rexp) {/*遍历所有fd*/   
   unsigned long in, out, ex, all_bits, bit = 1, mask, j;    
   unsigned long res_in = 0, res_out = 0, res_ex = 0;    
   const struct file_operations *f_op = NULL;    
   struct file *file = NULL;    

   in = *inp++; out = *outp++; ex = *exp++;    
   all_bits = in | out | ex;    
   if (all_bits == 0) {    
    /*   
    __NFDBITS定义为(8 * sizeof(unsigned long)),即long的位数。   
    因为一个long代表了__NFDBITS位,所以跳到下一个位图i要增加__NFDBITS   
    */   
    i += __NFDBITS;    
    continue;    
   }    

   for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {    
    int fput_needed;    
    if (i >= n)    
     break;    

    /*测试每一位*/   
    if (!(bit & all_bits))    
     continue;    

    /*得到file结构指针,并增加引用计数字段f_count*/   
    file = fget_light(i, &fput_needed);    
    if (file) {    
     f_op = file->f_op;    
     mask = DEFAULT_POLLMASK;    

     /*对于socket描述符,f_op->poll对应的函数是sock_poll   
     注意第三个参数是等待队列,在poll成功后会将本进程唤醒执行*/   
     if (f_op && f_op->poll)    
      mask = (*f_op->poll)(file, retval ? NULL : wait);    

     /*释放file结构指针,实际就是减小他的一个引用计数字段f_count*/   
     fput_light(file, fput_needed);    

     /*根据poll的结果设置状态,要返回select出来的fd数目,所以retval++。   
     注意:retval是in out ex三个集合的总和*/   
     if ((mask & POLLIN_SET) && (in & bit)) {    
      res_in |= bit;    
      retval++;    
     }    
     if ((mask & POLLOUT_SET) && (out & bit)) {    
      res_out |= bit;    
      retval++;    
     }    
     if ((mask & POLLEX_SET) && (ex & bit)) {    
      res_ex |= bit;    
      retval++;    
     }    
    }    

    /*   
    注意前面的set_current_state(TASK_INTERRUPTIBLE);   
    因为已经进入TASK_INTERRUPTIBLE状态,所以cond_resched回调度其他进程来运行,   
    这里的目的纯粹是为了增加一个抢占点。被抢占后,由等待队列机制唤醒。   

    在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),cond_resched是空操作   
    */     
    cond_resched();    
   }    
   /*根据poll的结果写回到输出位图里*/   
   if (res_in)    
    *rinp = res_in;    
   if (res_out)    
    *routp = res_out;    
   if (res_ex)    
    *rexp = res_ex;    
  }    
  wait = NULL;    
  if (retval || !*timeout || signal_pending(current))/*signal_pending前面说过了*/   
   break;    
  if(table.error) {    
   retval = table.error;    
   break;    
  }    

  if (*timeout < 0) {    
   /*无限等待*/   
   __timeout = MAX_SCHEDULE_TIMEOUT;    
  } else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {    
   /* 时间超过MAX_SCHEDULE_TIMEOUT,即schedule_timeout允许的最大值,用一个循环来不断减少超时值*/   
   __timeout = MAX_SCHEDULE_TIMEOUT - 1;    
   *timeout -= __timeout;    
  } else {    
   /*等待一段时间*/   
   __timeout = *timeout;    
   *timeout = 0;    
  }    

  /*TASK_INTERRUPTIBLE状态下,调用schedule_timeout的进程会在收到信号后重新得到调度的机会,   
  即schedule_timeout返回,并返回剩余的时钟周期数   
  */   
  __timeout = schedule_timeout(__timeout);    
  if (*timeout >= 0)    
   *timeout += __timeout;    
 }    

 /*设置为运行状态*/   
 __set_current_state(TASK_RUNNING);    
 /*清理等待队列*/   
 poll_freewait(&table);    

 return retval;    
}    

实例

int tcp_select(fd_set *readfds)
{
    // FD_SET(int fd, fd_set *fdset);       // 将fd加入set集合  
    // FD_CLR(int fd, fd_set *fdset);       // 将fd从set集合中清除  
    // FD_ISSET(int fd, fd_set *fdset);     // 检测fd是否在set集合中,不在则返回0  
    // FD_ZERO(fd_set *fdset);              // 将set清零使集合中不含任何fd 

    struct timeval tv;
    tv.tv_sec = 1;
    tv.tv_usec = 0;

    // int select(int nfds,  fd_set* readset,  fd_set* writeset,  fd_set* exceptset,  struct timeval* timeout);
    // nfds:readset、writeset、exceptset 3个数组中 socket 最大值+1,即告诉 fd_set 检查矢量图的第1位至第 maxfd+1 位。windows平台,该值可以设置不正确。
    // readset:select需要监视的可读 socket 的集合
    // writeset:select需要监视的可写 socket 的集合
    // exceptsetselect需要监视的异常 socket 的集合
    // timeout:本次 select 的超时结束时间,NULL:阻塞模式,0为非阻塞模式,其他为超时返回。timeval.tv_sec:秒,timeval.tv_usec:微秒
    // 返回值:错误返回 SOCKET_ERROR(-1),否则返回 可读、可写、异常 socket 的总数
    //
    // 函数功能:select会更新3个集合,把其中不可读、不可写、异常的 socket 去掉。 
    return select(0, readfds, NULL, NULL, &tv);
}

bool server(int port)
{
    WORD wVersionRequested = MAKEWORD(1, 1);    // 指定 Windows Sockets API 的版本号
    WSADATA wsaData;

    // 初始化 Winsock 服务
    // 返回值:成功返回0
    if (WSAStartup(wVersionRequested, &wsaData) != 0) 
    {
        return false;
    }

    // int socket(int af, int type, int protocol);
    // af:地址描述。AF_INET:IPv4 网络协议的套接字类型,其中 AF 表示 ADDRESS FAMILY 地址族
    // type:socket类型。TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。
    // protocol:协议。0:不指定。IPPROTO_TCP(TCP)、IPPROTO_UDP(UDP)、IPPROTO_STCP(STCP)、IPPROTO_TIPC(TIPC)。
    // 返回值:正确,返回新套接口的描述字,否则返回 INVALID_SOCKET
    int nServerSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (INVALID_SOCKET == nServerSocket)
    {
        return false;
    }

    // htons:将主机的无符号短整形数转换成网络字节顺序 
    // htonl:将主机的无符号长整形数转换成网络字节顺序
    /*
       网络字节序与主机字节序:
       字节序,顾名思义字节的顺序,即大于一个字节(长度超过8bit)的数据在内存中的存放顺序,而一个字节的数据没有顺序的问题。
       1、主机字节序即通常所说的大、小端模式,不同的CPU有不同的字节序类型(即,整数在内存中保存的顺序)。引用标准的Big-Endian和Little-Endian的定义如下: 
        a) Little-Endian:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。 
        b) Big-Endian:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。 

       2、网络字节序:TCP/IP中规定好的一种数据表示格式,采用大端模式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。
    */
    sockaddr_in ser;
    memset(&ser, 0, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(port); 
    ser.sin_addr.s_addr = htonl(INADDR_ANY);// 本机上所有 IP  或 inet_addr("192.168.2.71")

    // int PASCAL FAR bind(SOCKET s, const sockaddr* name,int namelen)
    // s:套接口的描述字
    // name:赋予套接口的地址
    // namelen:name名字的长度
    // 返回值:成功返回0,失败返回-1
    if (bind(nServerSocket, (sockaddr*)&ser, sizeof(ser))<0)
    {
        return false;
    }

    // int listen(SOCKET s, int backlog)
    // backlog:等待连接队列的最大长度
    // 返回值:成功返回0,失败返回-1。若队列已满,则客户将收到一个 WSAECONNREFUSED 错误(连接被拒绝)。
    if (listen(nServerSocket, 5)<0)
    {
        return false;
    }

    fd_set readfds;
    vector<int> vecSocket;
    vecSocket.push_back(nServerSocket);

    while (1)
    {
        FD_ZERO(&readfds);
        for (int nIndex = 0; nIndex < vecSocket.size(); nIndex++)
        {
            FD_SET(vecSocket.at(nIndex), &readfds);
        }

        int nReadyCount = tcp_select(&readfds);
        if (nReadyCount <= 0)
        {
            Sleep(10);
            continue;
        }

        for (int nIndex = 0; nIndex < vecSocket.size(); nIndex++)
        {
            int nReadySocket = vecSocket.at(nIndex);
            if (!FD_ISSET(nReadySocket, &readfds))
            {
                continue;
            }

            if (nReadySocket == nServerSocket)
            {
                // SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
                // addr:客户端地址信息
                // 返回值:正确返回客户端 socket,错误返回 INVALID_SOCKET
                int nClientSocket = accept(nServerSocket, NULL, NULL);              
                if (nClientSocket == INVALID_SOCKET)
                {
                    // error
                }
                else
                {
                    vecSocket.push_back(nClientSocket);
                }
            }
            else
            {
                char buffer[1024] = "";

                // ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen)
                // buf:接收数据缓冲区
                // len:缓冲区长度
                // flags:选项
                // from:源地址
                // fromlen:from缓冲区长度
                //
                // 返回值:正确返回接收到的字节数,失败返回-1
                int nSize = recvfrom(nReadySocket, buffer, sizeof(buffer), 0, NULL, NULL);
                if (nSize > 0)
                {
                    if (sendto(nReadySocket, "receive success", sizeof("receive success"), 0, NULL, NULL) <= 0)
                    {
                        // WSAGetLastError()
                    }
                }
            }
        }
    }
}

close()、shutdown()区别

  1. int close(int sockfd):
    将套接字的引用计数-1并立即返回,若引用计数<=0,则该套接字不能再作为read或write的第一个参数。

  2. int shutdown(int sockfd,int howto):
    不管套接字的引用计数是否为0,切断该套接字的所有连接
    该函数的行为依赖于howto:
    1)SHUT_RD:值为0,关闭连接的读。
    2)SHUT_WR:值为1,关闭连接的写。
    3)SHUT_RDWR:值为2,关闭连接的读和写。

为什么需要bind

  1. tcp/udp server为什么都必须bind IP和Port:若不bind,server将无法知道应该去哪一块网卡、哪一个Port监听连接(tcp)、接收数据等。

  2. 为什么client不需要bind:
    1)当client调用connect(tcp)、send(upd)等函数,系统自动bind ip/Port(1024-5000,1-1024是保留端口号),同时在报文中会包含client ip/port等信息。
    这样后续server就知道应该向client 哪一个ip/port发送数据,client也会在该端口等待数据到达。

2)若client绑定ip/port,则使用指定的ip/port发送数据。ip/port可同时指定/不指定或只指定其中一个(bind()会返回错误,但仍然可以继续发送数据),不指定的将由系统自动分配。
缺点:会因端口不能被重复占用导致一台机器上只能运行一个client程序。

3)udp在调用 recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) 接收数据时,
除非src_addr/addrlen填NULL,否则参数addrlen初始值必须指定为sizeof(sockaddr_in)或sizeof(sockaddr),否则recvfrom()直接返回错误10022(参数错误)或10014(地址错误)。

send/recv、sendto/recvfrom

ssize_t send(int sockfd, const void *buf, size_t len, int flags)
ssize_t recv(int sockfd, void *buf, size_t len, int flags)

ssize_t sendto(int sockfd, const void *buf, size_t len, int  flags,const struct sockaddr *dest_addr, socklen_t addrlen)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)

注:两者可以搭配使用

flags:
1)MSG_DONTROUTE:目的主机在本地网络上,发送数据不使用网关。(send函数使用的标志)
2)MSG_OOB:可以发送、接收带外数据。(带外数据:当通信一方有重要的数据需通知对方时,为了快速发送数据,协议一般不使用与普通数据相同的通道,而是使用另外的通道。)
3)MSG_PEEK:recv函数只从系统缓冲区中读取内容,而不清除系统缓冲区的内容。这样在下次读取的时候,依然是一样的内容。
4)MSG_WAITALL:阻塞recv函数,直至收到 len 个字节数据或发生错误。

send()流程:
(1)比较待发送数据的长度len和套接字s的发送缓冲的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;
(2)检查协议s的发送缓冲中的数据是否正在发送,若是,则等待协议把数据发送完,否则比较s的发送缓冲区的剩余空间和len;
(3)若len大于剩余空间大小,则send一直等待协议把s的发送缓冲中的数据发送完;
(4)若len小于剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里。

注:
1)并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区。
2)send函数把buf中的数据成功copy到s的发送缓冲后就返回了,但是这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。

recv()流程:
(1)等待s的发送缓冲中的数据被协议传送完毕,若协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;
(2)若接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。
当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中。

注:
若协议接收到的数据大于buf的长度,则需要调用多次recv函数才能把s的接收缓冲中的数据copy完。

udp应该只能使用sendto/recvfrom方式,因为无连接模式,所以需要通过这种方式指定sendto 终端或获取recvfrom 源端地址。

tcp/udp流程

  1. tcp:
    WSAStartup() –>> socket() –>> bind() –>> listen() –>> accept() –>> send()/recv()、sendto()/recvfrom() –>> closesocket()/WSACleanup()
    WSAStartup() –>> socket() –>> connect() –>> send()/recv()、sendto()/recvfrom() –>> closesocket()/WSACleanup()

  2. udp:
    WSAStartup() –>> socket() –>> bind() –>> sendto()/recvfrom() –>> closesocket()/WSACleanup()
    WSAStartup() –>> socket() –>> sendto()/recvfrom() –>> closesocket()/WSACleanup()

参考资料:
http://blog.csdn.net/martin_liang/article/details/9124911

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值