Netlink实现热拔插监控

udev的文档介绍:

1. dynamic replacement for /dev。作为devfs的替代者,传统的devfs不能动态分配major和minor的值,而major和minor非常有限,很快就会用完了。 udev能够像DHCP动态分配IP地址一样去动态分配major和minor。 
2. device naming。提供设备命名持久化的机制。传统设备命名方式不具直观性,像/dev/hda1这样的名字肯定没有boot_disk这样的名字直观。udev能够像DNS解析域名一样去给设备指定一个有意义的名称。 
3. API to access info about current system devices 。提供了一组易用的API去操作sysfs,避免重复实现同样的代码。

用户空间的程序与设备通信的方法,主要有以下几种方式

1. 内存映射

采用内存映射的方式,将内核地址映射到用户态。这种方式最直接,可以适用大量的数据传输机制。这种方式的缺点是很难进行“业务控制”,没有一种可靠的机制保障内核和用户态的调动同步,比如信号量等都不能跨内核、用户层使用。因此内存映射机制一般需要配合一种“消息机制”来控制数据的读取,比如采用“消息”类型的短数据通道来完成一个可靠的数据读取功能。

用write/read/mmap去操作/dev目录下对应的设备,这也是设备驱动程序提供的接口。像framebuffer等都是这样做的。

2.  ioctl机制

用ioctl函数去操作/dev目录下对应的设备,这是设备驱动程序提供的接口。像键盘、鼠标和触摸屏等输入设备一般都是这样做的。 ioctl机制可以在驱动中扩展特定的ioctl消息,用于将一些状态从内核反应到用户态。Ioctl有很好的数据同步保护机制,不要担心内核和用户层的数据访问冲突,但是ioctl不适合传输大量的数据,通过和内存映射结合可以很好的完成大量数据交换过程。但是,ioctl的发起方一定是在用户态,因此如果需要内核态主动发起一个通知消息给用户层,则非常的麻烦。可能需要用户态程序采用轮询机制不停的ioctl

3. 通过ioperm获取操作IO端口的权限,然后用inb/inw/ inl/ outb/outw/outl等函数,避开设备驱动程序,直接去操作IO端口。

上面的方法在大多数情况下,都可以正常工作,但是对于热插拨(hotplug)的设备,比如像U盘,就有点困难了,因为你不知道:什么时候设备插上了,什么时候设备拔掉了。这就是所谓的hotplug问题了。 其他一些方式比如系统调用必须通过用户态发起,proc方式不太可靠和实时,用于调试信息的输出还是非常合适的。通过前面的项目背景,我需要一种可以在内核态主动发起消息的通知方式,而用户态的程序最好可以采用一种“阻塞调用”的方式等待消息。这样的模型可以最大限度的节省CPU的调度,同时可以满足及时处理的要求,最终选择了netlink完成通信的过程。

Netlink机制

Netlink是一种采用socket通信的机制,用于linux内核和上层用户空间进行通信的一种机制,通过实践netlink最大的优点是可以实现“双向通信”,是内核向用户态发起通知的一种最好选择。

1. Netlink的通信模型和socket通信非常相似,主要要点如下:

  • netlink采用自己独立的地址编码,struct sockaddr_nl;
  • 每个通过netlink发出的消息都必须附带一个netlink自己的消息头,struct nlmsghdr;
  • 内核态的netlink的操作API和用户态完全不一样,后面再介绍;
  • 用户态的netlink操作完成采用socket函数,非常方便和简单,有TCP/UDP socket编程基础的非常容易上手。

2. Netlink的通信地址和协议

地址:

所有socket之间的通信,必须有个地址结构,Netlink也不例外。我们最熟悉的就是IPV4的地址了,netlink的地址结构如下:

struct sockaddr_nl  
{  
    sa_family_t nl_family;          //必须为AF_NETLINK或者PF_NETLINK  
    unsigned short  nl_pad;         //必须为0  
    __u32       nl_pid;             //通信端口  
    __u32       nl_groups;          //组播掩码  
};  

上面几个数据,最关键的是nl_family就对应IP通信中的AF_INET)和nl_pid
nl_pid就是一个约定的通信端口,用户态使用的时候需要用一个非0的数字,一般来说可以直接采用上层应用的进程ID(getpid())(不用进程ID号码也没事,只要系统中不冲突的一个数字即可使用)。对于内核的地址,该值必须用0,也就是说,如果上层通过sendto向内核发送netlink消息,peer addr中nl_pid必须填写0。

nl_groups用于一个消息同时分发给不同的接收者,是一种组播应用,一般写 0

协议:

本质上,nl_pid就是netlink的通信地址。除了通信地址,netlink还提供“协议”来标示通信实体,在创建socket的时候,需要指定netlink的通信协议号。每个协议号代表一种“应用”,上层可以用内核已经定义的协议和内核进行通信,获得内核已经提供的信息。具体支持的协议列表如下:
 

#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

协议的用途很好理解,比如我们单纯创建一个上层应用,通过和NETLINK_ROUTE协议通信,可以获得内核的路由信息。我需要利用netlink创建一个我自己的通信协议,因此我定义了一种新的协议。新协议的定义不能和内核已经定义的冲突,同时不能超过MAX_LINKS这个宏的限定,MAX_LINKS = 32。

总结: netlink采用协议号+通信端口的方式构建自己的地址体系。

 

用户态操作netlink socket

《 PF_NETLINK应用实例:NETLINK_KOBJECT_UEVENT的实现 》

#include <linux/netlink.h>
#include <linux/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/un.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>


#define UEVENT_BUFFER_SIZE 2048

static int init_hotplug_sock(void)
{
    int ret;
    int s = -1;
    const int buffersize = 1024;
    struct sockaddr_nl snl;

    bzero(&snl, sizeof(struct sockaddr_nl));
    snl.nl_family = AF_NETLINK;
    snl.nl_pid = getpid();
    snl.nl_groups = 1;

    // 第一个参数必须是PF_NETLINK或者AF_NETLINK,
    // 第二个参数用SOCK_DGRAM和SOCK_RAW都没问题,第 
    // 第三个参数就是netlink的协议号。
    s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
    if (s == -1) {
        perror("socket");
        return -1;
    }
    setsockopt(s, SOL_SOCKET, SO_RCVBUF, &buffersize, sizeof(buffersize));

    ret = bind(s, (struct sockaddr *)&snl, sizeof(struct sockaddr_nl));
    if (ret < 0) {
        perror("bind");
        close(s);
        return -1;
    }

    return s;
}

int main(int argc, char *argv[])
{
    int hotplug_sock = init_hotplug_sock();

    while (1) {    
        char buf[UEVENT_BUFFER_SIZE * 2] = { 0 };

        recv(hotplug_sock, &buf, sizeof(buf), 0);
        printf("%s\n", buf);

        /* USB 设备的插拔会出现字符信息,通过比较不同的信息确定特定设备的插拔,在这添加比较代码 */
    }

    return 0;
}


编译:
$ gcc -g hotplug.c -o hotplug
执行:

$ ./hotplug
remove@/devices/pci0000:00/0000:00:1d.7/usb2/2-1/2-1:1.0
remove@/devices/pci0000:00/0000:00:1d.7/usb2/2-1
add@/devices/pci0000:00/0000:00:1d.7/usb2/2-1
add@/devices/pci0000:00/0000:00:1d.7/usb2/2-1/2-1:1.0

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HarkerYX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值