Unix/Linux编程:Netlink机制

什么是Netlink通信机制

  • Netlink是linux提供的用于内核和用户态进程之间的通信方式。
    • 但是注意虽然Netlink主要用于用户空间和内核空间的通信,但是也能用于用户空间的两个进程通信。
    • 只是进程间通信有其他很多方式,一般不用Netlink。除非需要用到Netlink的广播特性时。
    • NetLink机制是一种特殊的socket,它是Linux特有的,由于传送的消息是暂存在socket接收缓存中,并不为接受者立即处理,所以netlink是一种异步通信机制。系统调用和ioctl是同步通信机制
  • 用户空间进程可以通过标准socket API来实现消息的发送、接收。在Linux中,很多用户空间和内核空间的交互都是通过Netlink机制完成的。在Linux3.0的内核版本中定义了下面的21个用于Netlink通信的宏,其中默认的最大值为32
#define NETLINK_ROUTE        0    /* Routing/device hook                */  
#define NETLINK_UNUSED        1    /* Unused number                */  
#define NETLINK_USERSOCK    2    /* Reserved for user mode socket protocols     */  
#define NETLINK_FIREWALL    3    /* Firewalling hook                */  
#define NETLINK_INET_DIAG    4    /* INET socket monitoring            */  
#define NETLINK_NFLOG        5    /* netfilter/iptables ULOG */  
#define NETLINK_XFRM        6    /* ipsec */  
#define NETLINK_SELINUX        7    /* SELinux event notifications */  
#define NETLINK_ISCSI        8    /* Open-iSCSI */  
#define NETLINK_AUDIT        9    /* auditing */  
#define NETLINK_FIB_LOOKUP    10      
#define NETLINK_CONNECTOR    11  
#define NETLINK_NETFILTER    12    /* netfilter subsystem */  
#define NETLINK_IP6_FW        13  
#define NETLINK_DNRTMSG        14    /* DECnet routing messages */  
#define NETLINK_KOBJECT_UEVENT    15    /* Kernel messages to userspace */  
#define NETLINK_GENERIC        16  
/* leave room for NETLINK_DM (DM Events) */  
#define NETLINK_SCSITRANSPORT    18    /* SCSI Transports */  
#define NETLINK_ECRYPTFS    19  
#define NETLINK_RDMA        20  
  
#define MAX_LINKS 32  

目前在Linux 内核中使用netlink 进行应用与内核通信的应用很多,包括:

  • 路由 daemon(NETLINK_ROUTE)
  • 用户态 socket 协议(NETLINK_USERSOCK)
  • 防火墙(NETLINK_FIREWALL)
  • netfilter 子系统(NETLINK_NETFILTER)
  • 内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)
  • 通用 netlink(NETLINK_GENERIC)。NETLINK_GENERIC是一个通用的协议类型,它是专门为用户使用的,因此,用户可以直接使用它,而不必添加新的协议类型。

Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。

  • 用户态可以使用标准的socket APIs,socket()、bind()、sendmsg()、recvmsg()和close()等函数就能够很容易的使用netlink socket,我们在用户空间可以直接通过socket函数来使用netlink通信,比如可以通过下面的方式:
sock = socket (AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);  

那么Netlink有什么优势呢?

  • 一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、Netlink。而前两种都是单向的,但是Netlink可以实现双工通信。

  • Netlink协议基于BSD socket和AF_NETLINK地址簇(address family),使用32位的端口号寻址(以前称为PID),每个Netlink协议(或称作总线,man手册中则称之为netlink family),通常与一个或者一组内核服务/组件相关联,如NETLINK_ROUTE用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT用于内核向用户空间的udev进程发送通知等。

netlink具有以下特点:

  • 支持全双工、异步通信
  • 用户空间可以使用标准的BSD socket接口(但netlink并没有屏蔽掉协议包的构造与解析过程,推荐使用libnl等第三方库)
  • 在内核空间使用专用的内核API接口
  • 支持多播(因此支持“总线”式通信,可实现消息订阅)
  • 在内核端可用于进程上下文与中断上下文

对于每一个netlink协议类型,可以使用多播的概念,最多可以有32个多播组,每一个多播组用一个位表示,netlink的多播特性是的发送消息给同一个组仅需要一次系统调用,因而对于需要多播消息的应用而言,大大降低了系统调用的次数。

Netlink 相对于系统调用,ioctl 以及 /proc文件系统而言具有以下优点:

  • netlink使用简单,只需要在include/linux/netlink.h中增加一个新类型的 netlink 协议定义即可,(如 #define NETLINK_TEST 20 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换);
  • netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息;
  • 使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖;
  • netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性;
  • 内核可以使用 netlink 首先发起会话;

常用宏

#define NLMSG_ALIGNTO   4U
/* 宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

/* Netlink 头部长度 */
#define NLMSG_HDRLEN     ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))

/* 计算消息数据len的真实消息长度(消息体 + 消息头)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)

/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))

/* 宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏 */
#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))

/* 宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址, 同时len 变为剩余消息的长度 */
#define NLMSG_NEXT(nlh,len)  ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
                  (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))

/* 判断消息是否 >len */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
               (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
               (nlh)->nlmsg_len <= (len))

/* NLMSG_PAYLOAD(nlh,len) 用于返回payload的长度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

用户态数据结构

首先看一下几个重要的数据结构的关系:
在这里插入图片描述

struct msghdr

struct iovec {                    /* Scatter/gather array items */
     void  *iov_base;              /* Starting address */
     size_t iov_len;               /* Number of bytes to transfer */
 };
  /* iov_base: iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,
    以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff)
  */
 struct msghdr {
     void         *msg_name;       /* optional address */
     socklen_t     msg_namelen;    /* size of address */
     struct iovec *msg_iov;        /* scatter/gather array */
     size_t        msg_iovlen;     /* # elements in msg_iov */
     void         *msg_control;    /* ancillary data, see below */
     size_t        msg_controllen; /* ancillary data buffer len */
     int           msg_flags;      /* flags on received message */
 };
 /* msg_name: 数据的目的地址,网络包指向sockaddr_in, netlink则指向sockaddr_nl;
    msg_namelen: msg_name 所代表的地址长度
    msg_iov: 指向的是缓冲区数组
    msg_iovlen: 缓冲区数组长度
    msg_control: 辅助数据,控制信息(发送任何的控制信息)
    msg_controllen: 辅助信息长度
    msg_flags: 消息标识
 */

msghdr这个结构在socket变成中就会用到,并不算Netlink专有的。这里说明一下如何更好理解这个结构的功能。

我们知道socket消息的发送和接收函数一般有这几对:recv/send、readv/writev、recvfrom/sendto。当然还有recvmsg/sendmsg,前面三对函数各有各的特点功能,而recvmsg/sendmsg就是要囊括前面三对的所有功能,当然还有自己特殊的用途。

msghdr的前两个成员就是为了满足recvfrom/sendto的功能,中间两个成员msg_iov和msg_iovlen则是为了满足readv/writev的功能,而最后的msg_flags则是为了满足recv/send中flag的功能,剩下的msg_control和msg_controllen则是满足recvmsg/sendmsg特有的功能。

struct sockaddr_ln

struct sockaddr_ln为Netlink的地址,和我们通常socket编程中的sockaddr_in作用一样,他们的结构对比如下:
在这里插入图片描述

struct sockaddr_nl的详细定义和描述如下:

struct sockaddr_nl
{
    sa_family_t nl_family; /*该字段总是为AF_NETLINK */
    unsigned short nl_pad; /* 目前未用到,填充为0*/
    __u32 nl_pid; /* process pid */
    __u32 nl_groups; /* multicast groups mask */
};

说明:

  • sa_family_t nl_family; //一般为AF_NETLINK,
  • unsigned short nl_pad; //字段 nl_pad 当前没有使用,因此要总是设置为 0
  • nl_pid
    • 在Netlink规范里,PID全称是Port-ID(32bits),其主要作用是用于唯一的标识一个基于netlink的socket通道。
    • 通常情况下nl_pid都设置为当前进程的进程号。前面我们也说过,Netlink不仅可以实现用户-内核空间的通信还可使现实用户空间两个进程之间,或内核空间两个进程之间的通信。该属性为0时一般指内核。
  • __u32 nl_groups;
    • 如果用户空间的进程希望加入某个多播组,则必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码
    • 如果该字段为0则表示调用者不希望加入任何多播组。对于每个隶属于Netlink协议域的协议,最多可支持32个多播组(因为nl_groups的长度为32比特),每个多播组用一个比特来表示。

NETLINK_ROUTE的多播组定义位于retnetlink.h,RTMGRP_*格式,这里列出常用的几个:

  • RTMGRP_LINK - 当网卡变动时会触发这个多播组,例如插拔网线、增减网卡设备、启用禁用接口等
  • RTMGRP_IPV4_IFADDR - 当ipv4地址变动时会触发这个多播组,例如修改IP
  • RTMGRP_IPV4_ROUTE - 当ipv4路由变动时会触发这个多播组
  • RTMGRP_IPV6_IFADDR - 当ipv6地址变动时会触发这个多播组,例如修改IP
  • RTMGRP_IPV6_ROUTE - 当ipv6路由变动时会触发这个多播组

struct nlmsghdr

Netlink的报文由消息头和消息体构成,struct nlmsghdr即为消息头。消息头定义在文件里,由结构体nlmsghdr表示:

struct nlmsghdr
{
    __u32 nlmsg_len; /* Length of message including header */
    __u16 nlmsg_type; /* Message content */
    __u16 nlmsg_flags; /* Additional flags */
    __u32 nlmsg_seq; /* Sequence number */
    __u32 nlmsg_pid; /* Sending process PID */
};

消息头中各成员属性的解释及说明:

  • nlmsg_len:整个消息的长度,按字节计算。包括了Netlink消息头本身。
    -nlmsg_type:消息的类型,即是数据还是控制消息。目前(内核版本2.6.21)Netlink仅支持四种类型的控制消息,如下:
    • NLMSG_NOOP:不执行任何动作,必须将该消息丢弃;
    • NLMSG_ERROR:指明该消息中包含一个错误;
    • NLMSG_DONE:标识分组消息的末尾;----如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。
    • NLMSG_OVERRUN:缓冲区溢出,表示某些消息已经丢失。
    • NLMSG_MIN_TYPEK:预留
  • nlmsg_flags:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI。
  • nlmsg_seq:消息序列号,用以将消息排队,有些类似TCP协议中的序号(不完全一样),但是netlink的这个字段是可选的,不强制使用;
  • nlmsg_pid:发送端口的ID号,对于内核来说该值就是0,对于用户进程来说就是其socket所绑定的ID号

用户空间Netlink socket API

1. 创建套接字

skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
  • 第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux 中,它们俩实际为一个东西,它表示要使用netlink,
  • 第二个参数必须是SOCK_RAW或SOCK_DGRAM, 因为netlink是一个面向数据报的服务;
  • 第三个参数指定netlink协议类型,它可以是一个自定义的类型,也可以使用内核预定义的类型

可以非常容易的添加自己的netlink协议。

为每一个协议类型最多可以定义32个多播组。

每一个多播组用一个bitmask来表示,1<<i(0<=i<= 31),这在一组进程和内核进程协同完成一项任务时非常有用。发送多播netlink消息可以减少系统调用的数量,同时减少用来维护多播组成员信息的负担。

2. 绑定套接字

bind(skfd, (struct sockaddr*)&local, sizeof(local))

local为netlink的socket地址,即上面提到的:

struct sockaddr_nl
{
  sa_family_t    nl_family;   // 成员 nl_family为协议簇 AF_NETLINK
  unsigned short nl_pad;    //成员 nl_pad 当前没有使用,因此要总是设置为 0
  __u32          nl_pid;    // 成员 nl_pid 为接收或发送消息的进程的 ID
  __u32          nl_groups;
};

例子

struct sockaddr_nl local;
memset(&local, 0, sizeof(local));
local.nl_family = AF_NETLINK;
local.nl_pid = getpid();    /*设置pid为自己的pid值*/
local.nl_groups = 0;

3.发送netlink消息

用户空间调用send函数(如sendto、sendmsg等)向内核发送数据,使用同样的socket地址来描述内核,不过需要注意,由于对端是内核,nl_pid必须设置为0。

内核的socket地址

struct sockaddr_nl kpeer;
memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0;
kpeer.nl_groups = 0;

为了发送一条netlink消息到内核或者其他的用户空间进程,另外一个struct sockaddr_nl nladdr需要作为目的地址,这和使用sendmsg()发送一个UDP包是一样的。

  • 如果该消息是发送至内核的,那么nl_pid和nl_groups都置为0.
  • 如果消息是发送给另一个进程的单播消息,nl_pid是另外一个进程的pid值而nl_groups为零。
  • 如果消息是发送给一个或多个多播组的多播消息,所有的目的多播组必须bitmask必须or起来从而形成nl_groups域。

用户进程想内核发送的数据包格式为:“netlink消息头 + 数据”,消息头描述为:

struct nlmsghdr
{
    __u32 nlmsg_len;   /* Length of message */
    __u16 nlmsg_type;  /* Message type*/
    __u16 nlmsg_flags; /* Additional flags */
    __u32 nlmsg_seq;   /* Sequence number */
    __u32 nlmsg_pid;   /* Sending process PID */
};

自定义消息首部,它仅包含了netlink的消息首部

struct msg_to_kernel
{
  struct nlmsghdr hdr;
};

填充首部信息

struct msg_to_kernel message;
memset(&message, 0, sizeof(message));     
message.hdr.nlmsg_len = NLMSG_LENGTH(0);   /*没有数据,所以长度为0.*/
message.hdr.nlmsg_flags = 0;
message.hdr.nlmsg_type = IMP2_U_PID;
message.hdr.nlmsg_pid = local.nl_pid;

向内核发送消息

sendto(skfd, &message, message.hdr.nlmsg_len, 0(struct sockaddr*)&kpeer, sizeof(kpeer));

4.接收netlink消息

当发送完请求后,就可以调用recv函数簇从内核接收数据了,接收的数据包含了netlink消息首部和自定义数据结构。

自定义的数据结构

一个接收程序必须分配一个足够大的内存用于保存netlink消息头和消息负载。然后其填充struct msghdr msg,再使用标准的recvmsg()函数来接收netlink消息。

struct u_packet_info
{
  struct nlmsghdr hdr;            /*netlink消息头*/
  struct packet_info icmp_info;
};
struct u_packet_info info;

接受和处理从内核接受到的信息

while(1)
{
      kpeerlen = sizeof(struct sockaddr_nl);
      /*接收内核空间返回的数据*/
      rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),0, (struct sockaddr*)&kpeer, &kpeerlen); 
       /*处理接收到的数据*/
        ……
}

5. 关闭socket

函数close用于关闭打开的netlink socket。程序中,因为程序一直循环接收处理内核的消息,需要收到用户的关闭信号才会退出

♦ 关闭套接字的工作放在了自定义的信号函数sig_int中处理

static void sig_int(int signo)
{
   struct sockaddr_nl kpeer;       /*内核的socket地址*/
   struct msg_to_kernel message;   /*自定义netlink消息首部*/
   memset(&kpeer, 0, sizeof(kpeer));
   kpeer.nl_family = AF_NETLINK;
   kpeer.nl_pid    = 0;
   kpeer.nl_groups = 0;
   memset(&message, 0, sizeof(message));
   message.hdr.nlmsg_len = NLMSG_LENGTH(0);
   message.hdr.nlmsg_flags = 0;
   message.hdr.nlmsg_type = IMP2_CLOSE;
   message.hdr.nlmsg_pid = getpid();
   /*向内核发送一个消息,由nlmsg_type表明,应用程序将关闭*/
   sendto(skfd, &message, message.hdr.nlmsg_len, 0,(struct sockaddr *)(&kpeer),sizeof(kpeer));
   close(skfd);
   exit(0);
}

内核空间Netlink socket API

当 netlink 套接字用于内核空间与用户空间的通信时,在用户空间的创建方法和一般套接字使用类似,但内核空间的创建方法则不同,下图是 netlink 套接字实现此类通信时创建的过程:

1.创建netlink套接字

通过netlink_kernel_create创建一个netlink套接字,同时,注册一个回调函数,用于接收处理用户空间的消息。

struct sock *netlink_kernel_create(struct net *net,
                                   int unit,unsigned int groups,
                                   void (*input)(struct sk_buff *skb),
                                   struct mutex *cb_mutex,struct module *module);

参数说明:

  • net:是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用 init_net这个全局变量。
  • unit:表示netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX。
  • groups:多播地址。
  • input:为内核模块定义的netlink消息处理函数,当有消 息到达这个netlink socket时,该input函数指针就会被引用,且只有此函数返回时,调用者的sendmsg才能返回。
  • cb_mutex:为访问数据时的互斥信号量。
  • module: 一般为THIS_MODULE。

2.发送单播消息 netlink_unicast

int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)

参数说明:

  • ssk:为函数 netlink_kernel_create()返回的socket。
  • skb:存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,宏NETLINK_CB(skb)就用于方便设置该控制块。
  • pid:为接收此消息进程的pid,即目标地址,如果目标为组或内核,它设置为 0。
  • nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回;而如果为0,该函数在没有接收缓存可利用定时睡眠。
static int send_to_user(struct packet_info *info)
{
    int ret;
    int size;
    unsigned char *old_tail;
    struct sk_buff *skb;
    struct nlmsghdr *nlh;
    struct packet_info *packet;
    /*计算消息总长:消息首部加上数据加度*/
    size = NLMSG_SPACE(sizeof(*info));
    /*分配一个新的套接字缓存*/
    skb = alloc_skb(size, GFP_ATOMIC);
    old_tail = skb->tail;
    /*初始化一个netlink消息首部,NLMSG_PUT(skb, pid, seq, type, len)*/
    nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
    /*跳过消息首部,指向数据区*/
    packet = NLMSG_DATA(nlh);
    /*初始化数据区*/
    memset(packet, 0, sizeof(struct packet_info));
    /*填充待发送的数据*/
    packet->src = info->src;
    packet->dest = info->dest;
    /*计算skb两次长度之差,即netlink的长度总和*/
    nlh->nlmsg_len = skb->tail - old_tail;
    /*设置控制字段*/
    NETLINK_CB(skb).dst_groups = 0;   /*如果它目标为某一进程或内核,dst_group 应当设置为 0。*/
    /*发送数据*/
    read_lock_bh(&user_proc.lock);
    ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
    read_unlock_bh(&user_proc.lock);
}

3.发送广播消息 netlink_broadcast

int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)

前面的三个参数与 netlink_unicast相同

  • 参数group为接收消息的多播组,该参数的每一个位代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。
  • 参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。

4. 接收处理用户空间发送的数据(kernel_receive 函数)

用户空间向内核发送了两种自定义消息类型:IMP2_U_PID和IMP2_CLOSE,分别是请求和关闭。kernel_receive 函数分别处理这两种消息:

DECLARE_MUTEX(receive_sem); /*初始化信号量*/
static void kernel_receive(struct sock *sk, int len)
{
     do
    {
           struct sk_buff *skb;
           if(down_trylock(&receive_sem))     /*获取信号量*/
                    return;
          /*从sk接收队列中取得skb,然后进行一些基本的长度的合法性校验*/
          while((skb = skb_dequeue(&sk->receive_queue)) != NULL) 
          {
                 {
                      struct nlmsghdr *nlh = NULL;
                      if(skb->len >= sizeof(struct nlmsghdr))
                     {
                             /*获取数据中的nlmsghdr 结构的报头*/
                             nlh = (struct nlmsghdr *)skb->data;
                             if((nlh->nlmsg_len >= sizeof(struct nlmsghdr))&& (skb->len >= nlh->nlmsg_len))
                            {
                                  /*长度的全法性校验完成后,处理应用程序自定义消息类型,主要是对用户PID的保存,即为内核保存“把消息发送给谁”*/
                                 if(nlh->nlmsg_type == IMP2_U_PID)/*请求*/
                                 {
                                        write_lock_bh(&user_proc.pid);
                                        user_proc.pid = nlh->nlmsg_pid;
                                        write_unlock_bh(&user_proc.pid);
                                 }
                                 else if(nlh->nlmsg_type == IMP2_CLOSE)/*应用程序关闭*/
                                 {
                                       write_lock_bh(&user_proc.pid);
                                       if(nlh->nlmsg_pid == user_proc.pid)
                                                      user_proc.pid = 0;
                                       write_unlock_bh(&user_proc.pid);
                                  }
                           }
                      }
                }
                kfree_skb(skb);
           }
           up(&receive_sem);     /*返回信号量*/
    }while(nlfd && nlfd->receive_queue.qlen);
}

实例

http://blog.chinaunix.net/uid-20788636-id-2980152.html
https://www.cnblogs.com/wenqiang/p/6306727.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值