赞
踩
linux开发中通常会涉及用户空间和内核模块的交互。以WiFi软件开发为例,hostapd系列、iw、cfg80211tool、iwpriv、iwconfig、ifconfig
等一系列WiFi相关的应用均会和内核模块产生通信和交互,而从通信方式上划分,通常可分为IOCTL
和Netlink
两种方式,如下所示:
ioctl
本质上为一种系统调用,是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、网口的配置等等。
和read/write
函数相比,他们都可以往内核中写数据,但是read
函数只能完成读的功能,write
函数只能完成写的功能,而ioctl
既可以读也可以写,当然在读取大数据时,ioctl
的效率不及read/write
函数。下文给出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) */
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
...
}
在用户空间使用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
本质上是socket
。它是一种IPC(Inter Process Commumicate)
机制,用于内核与用户空间通信的机制,同时它也以用于进程间通信(Netlink
更多用于内核通信,进程之间通信更多使用Unix域套接字)。
在一般情况下,用户态和内核态通信会使用传统的Ioctl
、sysfs
属性文件或者procfs
属性文件,这3种通信方式都是同步通信方式,由用户态主动发起向内核态的通信,内核无法主动发起通信。
而Netlink
是一种异步全双工的通信方式,它支持由内核态主动发起通信,内核为Netlink
通信提供了一组特殊的API接口,用户态则基于socket API
,内核发送的数据会保存在接收进程socket 的接收缓存中,由接收进程处理。
Netlink
具有以下优点:
ioctl
这类的单工方式)。如此用户空间在等待内核某种触发条件满足时就无需不断轮询,而异步接收内核消息即可。下文以hostapd
和内核的通信为例,对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
命令注册:nl80211
的main
函数注册和初始化了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,
},
...
}
如上,内核使用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函数
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库中函数,连接内核
基于新建的netlink socket
,hostapd
可以和驱动进行双向通信:
**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库中函数,接收
以上就是对IOCTL
和Netlink
实现和使用的介绍,在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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。