【WiFi软件开发】IOCTL和Netlink----用户空间和内核空间交互的两种方式


前言

linux开发中通常会涉及用户空间和内核模块的交互。以WiFi软件开发为例,hostapd系列、iw、cfg80211tool、iwpriv、iwconfig、ifconfig等一系列WiFi相关的应用均会和内核模块产生通信和交互,而从通信方式上划分,通常可分为IOCTLNetlink两种方式,如下所示:

  • IOCTL:hostapd系列(可以用ioctl进行开发),iwpriv,iwconfig,ifconfig
  • Netlink:hostapd系列(默认方式是netlink),iw,cfg80211tool等

一、IOCTL

ioctl本质上为一种系统调用,是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、网口的配置等等。

read/write函数相比,他们都可以往内核中写数据,但是read函数只能完成读的功能,write函数只能完成写的功能,而ioctl既可以读也可以写,当然在读取大数据时,ioctl的效率不及read/write函数。下文给出ioctl的实现和使用流程:

1.定义IOCTL命令

在内核模块中,需要使用宏定义你的IOCTL命令。通常情况下,IOCTL命令包括了一个命令编号、请求类型的方向(读/写/两者)以及数据大小:

**linux-5.4.196/include/uapi/linux/sockios.h**

/* Socket configuration controls. */
#define SIOCGIFNAME	0x8910		/* get iface name		*/
#define SIOCSIFLINK	0x8911		/* set iface channel		*/
#define SIOCGIFCONF	0x8912		/* get iface list		*/
#define SIOCGIFFLAGS	0x8913		/* get flags			*/
#define SIOCSIFFLAGS	0x8914		/* set flags			*/
#define SIOCGIFADDR	0x8915		/* get PA address		*/
#define SIOCSIFADDR	0x8916		/* set PA address		*/
#define SIOCGIFDSTADDR	0x8917		/* get remote PA address	*/
#define SIOCSIFDSTADDR	0x8918		/* set remote PA address	*/
#define SIOCGIFBRDADDR	0x8919		/* get broadcast PA address	*/
#define SIOCSIFBRDADDR	0x891a		/* set broadcast PA address	*/
#define SIOCGIFNETMASK	0x891b		/* get network PA mask		*/
#define SIOCSIFNETMASK	0x891c		/* set network PA mask		*/
#define SIOCGIFMETRIC	0x891d		/* get metric			*/
#define SIOCSIFMETRIC	0x891e		/* set metric			*/
#define SIOCGIFMEM	0x891f		/* get memory address (BSD)	*/
#define SIOCSIFMEM	0x8920		/* set memory address (BSD)	*/
#define SIOCGIFMTU	0x8921		/* get MTU size			*/
#define SIOCSIFMTU	0x8922		/* set MTU size			*/
#define SIOCSIFNAME	0x8923		/* set interface name */
#define	SIOCSIFHWADDR	0x8924		/* set hardware address 	*/
**linux-5.4.196/arch/mips/include/uapi/asm/sockios.h**

#define FIOGETOWN	_IOR('f', 123, int)
#define FIOSETOWN	_IOW('f', 124, int)

#define SIOCATMARK	_IOR('s', 7, int)
#define SIOCSPGRP	_IOW('s', 8, pid_t)
#define SIOCGPGRP	_IOR('s', 9, pid_t)

#define SIOCGSTAMP_OLD	0x8906		/* Get stamp (timeval) */
#define SIOCGSTAMPNS_OLD 0x8907		/* Get stamp (timespec) */

2.用户空间调用IOCTL

wld_linuxIfUtils_setMac(wld_rad_getSocket(pRad), intfName, macAddress); 	//sock最终调用socket(AF_INET, SOCK_DGRAM, 0);

int wld_linuxIfUtils_setMac(int sock, char* intfName, swl_macBin_t* macInfo) {
	...
    int ret = ioctl(sock, SIOCSIFHWADDR, &ifr);         	//调用ioctl   
	...
}

3.内核实现IOCTL的系统调用

在用户空间使用ioctl函数时,会使得系统从用户态trap到内核态,即调用到内核态的sys_ioctl函数。调用流程如下:

**linux-5.4.196/fs/ioctl.c**

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
	ksys_ioctl(fd, cmd, arg);
	    struct fd f = fdget(fd);			//获取fd
	    do_vfs_ioctl(f.file, fd, cmd, arg);
	        vfs_ioctl(filp, cmd, arg);
		        filp->f_op->unlocked_ioctl(filp, cmd, arg);         // .unlocked_ioctl = dev_ioctl,
			        int dev_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr, bool *need_copyout)     //  linux-5.4.196/net/core/dev_ioctl.c
				        dev_ifsioc(net, ifr, cmd);
					        dev_set_mac_address_user(dev, &ifr->ifr_hwaddr, NULL);	//对于SIOCSIFHWADDR命令
						        dev_set_mac_address(dev, sa, extack);
						        	const struct net_device_ops *ops = dev->netdev_ops;
						        	ops->ndo_set_mac_address(dev, sa);	//调用网卡驱动的net_device_ops结构体下的成员函数进行操作

上面讲的dev变量是struct net_device类型,而struct net_device在内核中表示我们的一个网卡驱动设备,注册该变量的文件都处于内核drivers/net目录下,通过register_netdev()内核函数来注册。

本质上来说,ioctl最终是调用file_operations->unlocked_ioctl,进而调用网卡驱动中注册的函数实现的。
(注:read/write函数本质上也是调用file_operations->write/read

二、Netlink

Netlink本质上是socket。它是一种IPC(Inter Process Commumicate)机制,用于内核与用户空间通信的机制,同时它也以用于进程间通信(Netlink 更多用于内核通信,进程之间通信更多使用Unix域套接字)。

在一般情况下,用户态和内核态通信会使用传统的Ioctlsysfs属性文件或者procfs属性文件,这3种通信方式都是同步通信方式,由用户态主动发起向内核态的通信,内核无法主动发起通信。

Netlink是一种异步全双工的通信方式,它支持由内核态主动发起通信,内核为Netlink通信提供了一组特殊的API接口,用户态则基于socket API,内核发送的数据会保存在接收进程socket 的接收缓存中,由接收进程处理。
Netlink具有以下优点:

  • 双向全双工异步传输,支持由内核主动发起传输通信,而不需要用户空间出发(例如使用ioctl这类的单工方式)。如此用户空间在等待内核某种触发条件满足时就无需不断轮询,而异步接收内核消息即可。
  • 支持组播传输,即内核态可以将消息发送给多个接收进程,这样就不用每个进程单独来查询了。

下文以hostapd和内核的通信为例,对Netlink的通信流程进行分析:

1.内核中初始化Netlink

创建netlink socket和接收函数:

**linux-5.4.196/net/netlink/genetlink.c**

static int __net_init genl_pernet_init(struct net *net)
{
	struct netlink_kernel_cfg cfg = {
		.input		= genl_rcv,											//定义接收函数
		.flags		= NL_CFG_F_NONROOT_RECV,
	};

	/* we'll bump the group number right afterwards */
	net->genl_sock = netlink_kernel_create(net, NETLINK_GENERIC, &cfg);		//创建netlink socket

	if (!net->genl_sock && net_eq(net, &init_net))
		panic("GENL: Cannot initialize generic netlink\n");

	if (!net->genl_sock)
		return -ENOMEM;

	return 0;
}

netlink命令注册:nl80211main函数注册和初始化了nl80211_fam结构,其中就包含了op字段,值为nl80211_ops。之后又初始化了struct genl_ops nl80211_ops[],数组定义了命令和对应的钩子函数。上层通过netlink socket通信发送命令,nl80211中执行对应的函数。

**linux-5.4.196/net/wireless/nl80211.c**

static const struct genl_ops nl80211_ops[] = {
	...
	{
		.cmd = NL80211_CMD_SET_CHANNEL,
		.validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP,
		.doit = nl80211_set_channel,
		.flags = GENL_UNS_ADMIN_PERM,
		.internal_flags = NL80211_FLAG_NEED_NETDEV,
	},
	...
}

2.内核中接收消息

如上,内核使用socket建立时声明的接收函数.input = genl_rcv,进行消息接收,最终调用nl80211_ops表中注册的函数,流程如下:

**linux-5.4.196/net/netlink/genetlink.c**

static void genl_rcv(struct sk_buff *skb)
	netlink_rcv_skb(skb, &genl_rcv_msg);
		genl_family_rcv_msg(family, skb, nlh, extack);
			err = ops->doit(skb, &info);				//执行nl80211_ops表中对应的doit函数

3.hostapd中Netlink的初始化

hostapd中想要通过netlink发送消息,同样需要先对netlink进行初始化:

**hostapd-2022-07-29-b704dc72/src/drivers/driver_nl80211.c**

nl80211_global_init
    global->netlink = netlink_init(cfg);                            //  struct netlink_data *netlink;
        netlink->sock = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
        bind(netlink->sock, (struct sockaddr *) &local, sizeof(local))
        eloop_register_read_sock(netlink->sock, netlink_receive, netlink, NULL);
    wpa_driver_nl80211_init_nl_global(global)
        global->nl = nl_create_handle(global->nl_cb, "nl");         //创建新的 struct nl_sock *nl;
            handle = nl_socket_alloc_cb(cb);						//创建新的 struct nl_sock *nl;
            genl_connect(handle)									//连接内核
                nl_connect(sk, NETLINK_GENERIC);					//调用libnl库中函数,连接内核

4.hostapd中消息的发送

基于新建的netlink sockethostapd可以和驱动进行双向通信:

**hostapd-2022-07-29-b704dc72/src/drivers/driver_nl80211.c**

nl80211_set_channel
    msg = nl80211_drv_msg(drv, 0, set_chan ? NL80211_CMD_SET_CHANNEL : NL80211_CMD_SET_WIPHY);
    ret = send_and_recv_msgs(drv, msg, NULL, NULL, NULL, NULL);
    	/*通过建立的socket drv->global->nl发送消息*/
        send_and_recv(drv->global, drv->global->nl, msg, valid_handler, valid_data, ack_handler_custom, ack_data);			
            setsockopt
            setsockopt
            err = nl_send_auto_complete(nl_handle, msg);		//调用libnl库中函数,发送msg
            res = nl_recvmsgs(nl_handle, cb);					//调用libnl库中函数,接收

总结

以上就是对IOCTLNetlink实现和使用的介绍,在linux和WiFi开发中我们会经常接触到这两种方式用于用户空间和内核的通信。

学习链接:

构建自己的ioctl:
https://blog.csdn.net/eidolon_foot/article/details/135575367
https://cloud.tencent.com/developer/article/1431907
https://blog.csdn.net/qq_32276547/article/details/130181646
https://blog.csdn.net/u010571709/article/details/117632568

构建自己的netlink:
https://blog.csdn.net/eidolon_foot/article/details/135575367
https://blog.csdn.net/cleanfield/article/details/135952862

系统调用:
https://blog.csdn.net/weixin_45264425/article/details/136820917
https://blog.csdn.net/qq_43142509/article/details/124600228

file_operations文件操作结构体:
https://zhuanlan.zhihu.com/p/666583468
https://blog.csdn.net/yusiguyuan/article/details/11352155

其他:
https://zhuanlan.zhihu.com/p/703570419
https://blog.csdn.net/shujuliu818/article/details/122491924
https://blog.csdn.net/zhoucl123/article/details/131528131

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值