查改内核有如下几种方式:
1、 直接通过文件系统(procfs/sysfs);
2、 增加自己的系统调用;
3、 使用统一系统调用(ioctl);
4、 netlink;
第一种方式的好处是无需增加用户态代码,直接通过cat或echo可配置,但缺点是在内核里要是加太多比较乱,另外,总在linux shell下操作在某些场景不适合;
第二种的问题是加系统调用本身就比较麻烦,导致管理起来复杂;
第三种有效解决了第二种的问题,可以集中系统调用,但还是系统调用,这种用户态主动发起系统调用(类似查询的方式),效率比较低;
第四种方式实现了类似中断的方式,相对第二、三种形成了同步和异步的差异(注意netlink方式的收发是用户进程和内核,而第二、三种不存在收发双方,只是用户进程自己),明显提高效率,另外ioctl系统调用仅linux特有这将不利于程序移植,不过netlink使用起来更复杂;
综上所述,对于配置条目较少的情况下用第一、三种比较合适,对于配置条目较多的情况下netlink方式比较合适,至于第二种在某些特殊的情况会比较方便(OPL)。
与ioctl不同,netlink是基于socket,并且是内核和用户进程都创建各自的socket,这样双方都使用基于socket的收发缓冲队列,这一点最大的好处是内核和用户进程都可以发起向对方发送消息,准确的说用户进程和内核进程(内核在系统中事实上也可以看做一个进程,其进程tgid为0)都可以是消息的发送者,而ioctl只能是用户进程主动对内核,从始至终都只是用户进程在运行。这避免了进程(包括内核)的盲目轮询,真正实现了双向异步访问,是访问效率的的本质性提高。
此外netlink还可以进行“组播”,即可以给多个其他进程(包括内核)发送消息。另外通过某些方式还可以实现netlink套接字的CPU间传输,不过应该没有这样的使用。
了解一种类型套接字的根本在于了解它的“N元组”(如IPV4套接字的五元组源目的地址和端口+传输层协议类型、原始套接字的二元组以太网类型和收发接口),这样就能确定访问对象;netlink套接字的“N元组”是进程id和协议类型,进程id是该netlink套接字创建进程的tgid(在同协议情况下会有所调整),协议类型包括系统默认创建的类型,也可自定义,使用netlink的二元组即可唯一的确定一个netlink对象。2.10代码的netlink协议类型如下:
Netlink协议类型最多可定义到32种,上图中的NETLINK_CTRL(17)、NETLINK_KDNS(20)、NETLINK_KCAP(21)为自定义的类型,其中最重要的是NETLINK_CTRL,这是2.10代码中配置管理内核的ctrl_rtnetlink模块定义的netlink协议类型。
在系统中netlink的实际使用情况如下图所示:
第二列的“Eth”标识协议类型,第三列的“Pid”标识进程tgid,注意同一进程下创建不止一个的相同类型的netlink套接字时,“Pid”会在bind时修改为由-4097依次减小的值,至于为什么上图出现的负值不按顺序是因为某些进程在创建完netlink并使用后又把netlink释放了所致。注意“Pid”为924的是pon进程创建的netlink套接字。
netlink套接字的创建和其他类型套接字道理一样,需要注意的是用户进程和内核创建netlink的区别,区别并不大,内核netlink的创建会在sock层描述符(netlink_sock)中加入标志位(NETLINK_KERNEL_SOCKET)来标识该netlink套接字是内核创建的,此外内核创建的netlink会挂接其报文处理函数(netlink_rcv)来处理。
对于netlink套接字必须bind,描述bind之前首先描述netlink套接字的管理方式,在系统中根据不同的协议类型创建32个hash表(nl_table),hash表以其pid为基值计算hash键值,每个hash条目包括netlink套接字的创建进程pid(tgid)和其sock层描述符,如下图:
netlink套接字的bind过程就是把创建netlink的进程(包括内核)的tgid及其sock层描述符注册进系统(这和IPV4和原始套接字的道理是一样的),具体就是即注册进系统netlink管理链表nl_table,这样,当发送报文时,系统可以根据接收方的tgid得知接收方的sock层描述符,进而知道将报文发往哪个socket接收缓冲队列,否则接收方无法收到报文。
内核向用户进程发送
用户进程发送
事实上netlink报文没有固定规定的格式,但是内核默认创建的netlink(如路由使用的rtnetlink,netfilter使用的nfnetlink)都遵循一个固定的报文格式,所以自定义的netlink协议类型也应该遵循该格式,如下:
struct req
{
struct nlmsghdr nlh;
char buf[XXXXX];
};
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 port ID */
};
netlink报文首先应该是一个nlmsghdr结构的报文头,指定报文长度、消息类型(API ID)、报文标志(指示报文的需求,如是否需要回复、是否是请求、设置型还是打印型等)、报文序号(用于检验再收到的报文是否是本次请求的回应)、发送者pid(无意义未被应用);重点需注意的是报文标志,内核规定了如下几种报文标志:
不同的报文标志的处理都可以在实际代码中再理解其用途和含义,这里只介绍下最基本的如NLM_F_REQUEST标识这是一个请求报文、NLM_F_ACK标识这是一个需要回复的报文、NLM_F_DUMP标识这个报文应该首先调用显示hook,注意每个netlink的hook遵循如下数据结构,它应是“设置型”或“显示型”的hook:
不一定肯定都要实现两种类型的hook,这完全根据具体实现。
2.10代码中所有业务均通过netlink配置管理,在内核空间自定义了私有的netlink协议类型NETLINK_CTRL(见2.2.1.1),见rcios/include/ctrlnetlink.h文件中定义的枚举类型,涵盖路由、安全、语音、pon、设备管理等多种业务类型,挂接各类业务的hook处理函数;在用户空间,以库函数方式封装了netlink访问内核的函数方法,由各个业务进程统一使用。
此外用户还以其他netlink协议类型与内核一些模块进行交互,如ipsec部分使用了协议类型NETLINK_XFRM,路由和dhcp部分使用了协议类型NETLINK_ROUTE等。
下面依次举两个例子分别说明用户进程配置内核资源和内核异步通知用户进程:
具体过滤逻辑并不复杂,在一个hash表中可配置是否过滤、mac过滤表,当执行过滤netfilter时,若使能过滤则检查mac过滤表即可,具体实现不详细描述。这里的重点是配置mac filter模块的过程,2.10设备通过命令行和web为用户配置接口,分别如下图:
如上图,命令行发送netlink报文的消息类型是CTRL_MACADDR_FILTER_ADDR,并要求接收者回复该报文(含义报文标志NLM_F_ACK);它在内核模块中对应的条目的“设置型”hook函数是ctrlnetlink_macaddr_handle,它将实现把命令行中MAC地址加入MAC过滤表。
上图是命令行下的配置过程,web配置在xml_XXXXX.c文件中,道理相同过程略。
GPON的PLOAM报文实际在内核驱动中接收处理,SDK并未提供用户空间接收PLOAM报文,为实现故障定位接收PLOAM报文,可以使用ioctl方式获取不仅比较麻烦,还易出问题,因为PLOAM互通很快,用iotcl的方式还必须暂存收到的所有PLOAM报文等待用户主动获取,而使用netlink方式主动发给用户空间则是最优选择,方式如下:
1、 pon进程启动后创建NETLINK_CTRL类型netlink套接字并bind;
2、 把pon进程tgid作为内容发送给内核netlink套接字;
3、 pon进程创建线程监听第一步创建的netlink套接字;
4、 每当内核gpon驱动收到PLOAM报文则发送给pon进程tgid对应的sock接收缓冲队列并唤醒pon进程去接收;