深入理解Linux网络技术内幕(三)——用户空间与内核的接口

发布于:2022-11-27 ⋅ 阅读:(661) ⋅ 点赞:(0)


前言

简要的介绍用户空间应用程序可用于与内核通信或读取内核输出信息的主要机制。不谈其实现细节,因为每种机制都需要一章才能说清楚。主要的目的在于给出足够的引导。通过本章你可以取得所需要的信息,了解给定目录如何添加到/proc及所在位置、处理给定ioctl命令的内核处理函数,以及目前用户空间网络配置最佳接口Netlink提供了哪些函数。
本章所关注的机制,就是在谈到用户空间配置命令(如ifconfig和route)及内核处理函数(应用所请求的配置)两者的接口。

概论

内核通过各种不同的接口把内部信息输出到用户空间。除了程序员用于请求特定信息的经典系统调用(system call)外,还有三个特殊接口,而其中两个是虚拟文件系统procfs(/proc文件系统)与sysctl(/proc/sys目录)

procfs(/proc文件系统)

这是一个虚拟文件系统,通常是挂(mount)在/proc,允许内核以文件的形式向用户空间输出内部信息。这些文件并没有实际存在于磁盘中,但是可以通过cat或more以及>shell重定向符予以写入。这些文件甚至可以像真实文件一样指定其访问权限。因此创建这些文件的内核组件可以说明任何一个文件可由谁读取或写入。目录则不能被写入(也就是说,用户不能把文件或目录添加到/proc中的任何目录,也不能从中予以移除)。
多数Linux发行套件的默认内核都支持procfs。procfs不能编译为一个模块。配置菜单中相关的内核选项为"Filesystems-->pseudo filesystems-->/proc file system support"。

sysctl(/proc/sys目录)

此接口允许用户空间读取或修改内核的变量的值。不能用此接口对每个内核变量进行操作:内核应明确指出那些变量从此接口是可见的。从用户空间,你可以用两种方式访问sysctl输出的变量。一种是sysctl系统调用(参见man sysctl),而另一种是procfs。当内核支持procfs时,会在/proc中添加一个特殊目录(/prco/sys),为每个由sysctl所输出的内核变量引入一个文件。
procps包随附的sysctl命令可用于配置由sysctl接口所输出的变量。此命令通过写入/proc/sys与内核对话。多数Linux发行套件默认内核都支持sysctl。sysctl不能编译为一个模块。配置菜单中相关的内核选项为"General setup --> Sysctl support"

sysfs(/sys文件系统)

procfs和sysctl已经被滥用很多年了,这就导致引入了一种新的文件系统:sysfs。sysfs以非常干净而有组织的方式输出很多信息。如你期望的,当前由sysctl所输出的部分信息可以移植到sysfs。
内核对sysfs的支持只是在2.6版才开始。多数Linux发行套件默认内核都支持sysfs。sysfs不能被编译为一个模块。配置菜单中相关的内核选项为"Filesystems --> Pseudo filesystems --> sysfs filesystem support(NEW)"。只有先开启"General setup --> Configure standerd kernal feature(for small systems)"选项,才能看得到上述选项。

ioctl系统调用

ioctl(输入/输出控制)系统调用操作对象是一个文件,通常是用于实现特殊设备所需但标准文件系统没有提供的操作。你也可以把socket系统调用返回的套接字描述符(socket description)传给ioctl,而这也是网络代码中使用ioctl的方式。此接口也由老一代(old-generation)命令所使用,如ifconfig和route等。

Netlink套接字(socket)

这是网络应用程序与内核通信时最新的首选机制。IPROTE2包中大多数命令都使用此接口。对Linux而言,Netlink代表的就是BSD世界中的路由套接字(routing scoket)。多数网络内核功能都可以用Netlink或ioctl接口进行配置,因为内核支持较新的配置工具(IPROUTE2)以及老式工具(ifconfig、route等)。

procfs与sysctl

procfs和sysctl都输出内核内部信息,但是procfs主要输出只读数据,而大多数sysctl信息都是可以写入的,但只有超级用户(superuser)能写入。
就只读数据而言,procfs和sysctl之间的选择依赖于有多少信息应该输出。与一个简单的内核变量或数据结构相关联的一些文件,可以用sysctl输出。其他涉及更为复杂的数据结构而且需要特殊格式时,就以procfs输出,例如缓存和统计数据。

procfs

大多数网络功能在其初始化时都会在/proc中注册一个或多个文件,不是在引导时就是在模块加载时。当一位用户读取该文件时,会引起内核间运行一组内核函数,以返回某种输出内容。网络代码所注册的文件位于/proc/net

/proc中的目录可以使用proc_mkdir创建。/proc/net中的文件可以使用在include/linux/proc_fs.h中的proc_net_fops_create和proc_net_remove予以注册和除名。这两个函数都是包裹函数,其中含有通用的API:create_proc_entry和remove_proc_entry。特别地,proc_net_fops_create负责创建文档(用proc_net_create),然后初始化其文件操作处理函数。我们来看一个实例。
以下是ARP协议如何在/proc/net中注册其arp文件:

static struct file_operations arp_seq_fops = {
		.owner    =  THIS_MODULE,
		.open     = arp_seq_open,
		.read     = seq_read,
		.llseek   = seq_lseek,
		.release  = seq_release_private,
};
static int __int arp_proc_init(void)
{
	if (!proc_net_fops_create("arp",S_IRUGO,&arp_seq_fops))
			return ENOMEM;
	return 0;
}

proc_net_fops_create的三个输入参数可知,文件名为arp,权限必须指定为只读,而且该组文件操作处理例程是arp_seq_ops。当一个用户读取该文件时,使用file_operations数据结构,允许procfs返回相当多(chunk)的数据给用户。当数据由一群相同类型的对象组成时会很有用。例如,ARP缓存在返回时一次只返回一项(entry),而路由表再返回时只返回一条路径。

open所初始化的例程(前例中的arp_seq_open)会做另一次重要的初始化:注册一个函数指针数组,包括procfs用于遍历要传回给用户数据的所有例程:一个例程启动倾卸(dump),另一个推进到一个项目,而另一个再倾卸一个项目。这些例程内部负责保存必要的环境信息(此例中,就是ARP缓存已经倾卸了多少),这些信息会就是倾卸点以及从正确位置重新继续倾卸所必需的。

static struct seq_operations arp_seq_ops = {
		.start    = clip_seq_start,
		.next     = neigh_seq_next,
		.stop     = neigh_seq_stop,
		.show     = neigh_seq_show,
};
static int arp_seq_open(struct inode *inode, struct file *file)
{
		...
		rc = seq_open(file, &arp_seq_ops);
		...
}

sysctl:目录 /proc/sys

用户在/proc/sys下看到的一个文件,实际上是一个内核变量。就每个变量而言,内核可以定义:

  • 要将其放在/proc/sys何处。与相同内核组件或功能相联系的变量,通常都位于同一个目录中。例如,在/proc/sys/net/ipv4里可以找到与IPv4相关的文件。
  • 命名。多数时候,文件名只是简单地命名为与相关连的内核变量相同的命令,但是,有时候为了更友好一点,其名字也会改变。
  • 访问权限。例如,一个文件可以由任何人读,但是只能由超级用户修改。

输出到/proc/sys中的变量内容可借助于相关联的文件进行读写(只要你有必要的权限),或者直接用sysctl系统调用。
有些目录和文件在引导期间静态定义,而其他的则是在运行期间添加的。导致运行期间创建目录或文件的实例如下:

  • 当一个内核模块实现一项新功能,或者有一个协议被加载或卸载(unload)时。
  • 当一个新的网络设备被注册或除名时。有些配置参数(如/proc/sys中的文件)对每个设备而言都有一个实体。例如,/proc/sys/net/ipv4/conf/proc/sys/net/ipv4/neigh目录对每个已注册的网络设备而言,其下都有一个子目录。

/proc/sys中的文件和目录都是以ctl_table结构定义的。ctl_table结构注册和除名通过在kernel/sysctl.c中定义的register_sysctl_table和unregister_sysctl_table函数完成。
以下是ctl_table的关键字段:
const char *procname——在/proc/sys中所用的文件名。
int maxlen——输出的内核变量的尺寸大小。
mode_t mode——分派给/proc/sys中相关联的文件或目录的访问权限
ctl_table *child——用于建立目录与文件之间的父子(parent-child)关系。
proc_handler——当你在/proc/sys中读取或写入一个文件时,完成读取或写入操作的函数。所有与文件(就是树的叶子)相关联的ctl_instances都必须由proc_handler初始化。内核会给目录分派一个默认值。
straegy——此函数可以选择初始化为在显示或存储之前,完成数据的额外格式化工作的函数。当/proc/sys中的文件用sysctl系统调用时,此函数就会被调用。
extra1
extra2——两个可选参数,通常用于定义变量的最小值和最大值。通常把这两个参数称为min/max参数。

根据与文件相关联的变量种类而定,proc_handler和strategy的初始化也会不同。例如,当内核变量由一个或多个整数值组成时,proc_dointvec就是proc_handler所用的函数。
下表列出了可用于初始化proc_handler和strategy的一些函数。所有函数的定义和充分的注释都在kernel/sysctl.c中。
在这里插入图片描述在这里插入图片描述strategy或proc_handler函数初始化为包含上面的函数之一的包裹函数,这并非不常见。包裹函数根据相关的内核变量的意义,附加一些逻辑或合理性检查。因任何功能配置而着眼于procfs接口时,为了简洁起见,将总是涉及proc_handler函数。

ctl_table初始化的实例

我们首先看文件和目录所用的ctl_table结构的初始化是什么,然后再看其如何使用。
/proc/sys/net/ipv4/conf/default/forwarding文件所用的ctl_table实体的初始化定义在net/ipv4/devinet.c中,如下所示

{
		.ctl_name   = NET_IPV4_CONF_FORWARDING,
		.procname   = "forwarding",
		.data       = &ipv4_devconf.forwarding,
		.maxlen     = sizeof(int),
		.mode       = 0644,
		.proc_handler = &devinet_sysctl_forward,
}

从上述的简单介绍看不出这个文件被放在/proc/sys何处。等一下你会看到如何找到这些信息。从代码可知,该文件名称为forwarding,通过forwarding文件输出其值的内核变量是ipv4_devconf.forwarding(此字段内有一个相当复杂的结构),参数声明为整数,该文件的访问权是0644(也就是任何人都可读,但是只有超级用户可以写入),而proc_handler函数被初始化为devinet_sysctl_forward。

现在,我们看kernel/sysctl.c中一个目录的声明实例:

{
		.ctl_name    = CTL_NET,
		.proname     = "net",
		.mode        = 0555,
		.child       = net_table, 
}

这个ctl_table实体定义了/proc/sys/net目录。此时,不需要proc_handler(内核提供默认函数,适用于所有目录的需要),但却有一个child字段。child是一个指针,指向另一个ctl_table实体,这个实体只是ctl_table实体列表的头元素(对于每一个文件或子目录都会在net目录中建立一个实体)。

在/proc/sys中注册文件

我们知道可以分别用register_sysctl_tableunregister_systl_table/proc/sys中注册或除名文件。注册函数(源码中的文档很详细)需要两个输入参数:

  • 指向一个ctl_table实体的指针
  • 一个标识,指出新元素应放在位于相同目录中ctl_table实体列表的何处:头或尾
    注意,register_sysctl_table的输入并不包括输入参数ctl_table应该添加到/proc/sys文件系统中何处的参考值。原因在于所有的插入都是针对/proc/sys目录进行的,如果你想要把一个文件注册到/proc/sys的子目录,就必须建立一棵树(意味着多个由child字段链接的ctl_table实体)以提供完整路径,然后把代表你刚健的树根的ctl_table实体传给register_sysctl_table。当该树的任何节点尚未存在时,就会被创建。

我们看两个实例,先从简单的入手。下面这段代码源自drivers/scsi/scsi_sysctl.c显示出文件logging_level的定义以及如何置放到/proc/sys/dev/scsi目录:

 static ctl_table scsi_table[] = {
		{  .ctl_name   = DEV_SCSI_LOGGING_LEVEL,
			 .procname   = "logging_level",
			 .data       = &scsi_logging_level,
			 .maxlen     = sizeof(scsi_logging_level),
			 .mode       = 0644,
			 .proc_handler = &proc_dointvec},
			 {}
};

static ctl_table scsi_dir_table[] = {
	{  .ctl_name   = DEV_SCSI,
		 .procname   = "scsi",
		 .mode       = 0555,
		 .child      = scsi_table
	},
	{}
};
int__init scsi_init_sysctl(void)
{
	scsi_table_handler = register_sysctl_table(scsi_root_table,1);
}

注意,register_sysctl_table接收的是scsi_root_table,也就是代码中所定义的ctl_table的树的根。如下图所示
在这里插入图片描述还注意到,如果稍后你想把另一个文件添加到同一个目录,例如abc文件,你需要定义一棵类似的树(也就是dev和scsi目录是两个相同的ctl_table实体,为新文件abc另加一个新的ctl_table实体)。

有时候,开发人员为了简化把新文件添加到已存在的目录而定义一个模板,然后,每次有新文件要添加到相同目录时就予以重用。使用模板的好处是ctl_table实体只需要初始化一次便可贯穿整个目录(例如,前例的scsi_root_table和scso_dir_table)。之后,每次增加一个新文件时,只需要对叶节点初始化(也就是真实的文件)。例如,你可去看看邻居子系统如何使用在net/core/neighbour.c中的neigh_sysctl_register定义neigh_sysctl_template。(后面再分析)

核心网络文件和目录

下图显示了在/proc/sys中由网路代码所使用的主要目录。就每个目录后面的博客会对应分析。
在这里插入图片描述对于上图所示的每个目录以及这些目录的每个文件而言,都有一个ctl_table实体。下图显示了上图大多数目录的ctl_table实体在何处定义以及其父子关系是什么。
在这里插入图片描述三个方块显示了ctl_table初始化的三个实例。注意

  • netdev_max_backlog文件被分派了一个proc_handler例程,但没有一个strategy例程。因为netdev_max_backlog是一个整数,来自用户的输入由proc_dointdev读取。
  • min_delay文件被分派了proc_handler和strategy两个例程。因为内核变量ip_rt_min_delay以jiffies表示,但是用户的输入和输出都是以秒来表示的,因此这两个例程可以完成把秒转换为jiffies。
  • ip_local_port_range文件是一个有趣案例。这个文件允许用户配置一个范围,定义两个值。这个范围必须遵循一个最小值和一个最大值的规范。因此,所选的strategy和proc_handler例程必须能够管理一个整数值的数组(此例中为两个整数值)。这些值(extra1和extra2)表示这个范围,并且用于确保用户的输入值遵循此范围。

ioctl

在这里插入图片描述
从此图的顶端,我们可以看到ioctl调用是如何发布(issue)的。来看一个包括ifconfig的示例。

之前我们说过,ifconfig命令使用ioctl与内核进行通信。例如,当系统管理输入像ifconfig eth0 mtu 1250这样的命令,用以改变接口eth0的MTU时,ifconfig会打开一个套接字,用从系统管理员哪里接收的信息(例中的data)初始化一个本地数据结构,然后以ioctl调用传给内核。SIOCSIFMTU是命令标识符。

	struct ifreq data;
	fd = socket(PF_INET, SOCKET_DGRAM, 0);
	< ... 对“data”初始化 ...>
	err = ioctl(fd, SIOCSIFMTU, &data)

内核会在几个不同地方处理ioctl命令。上图展示了网络代码最常用的ioctl命令如和由sock_ioctl分派,并且路由至正确的函数处理例程。我们将不说明sock_ioctl是如何调用的,或者像UDP和TCP这些传输协议是如何注册其处理例程的。如果你想深入挖掘这部分代码,可以使用此图最为起点。

网络用的ioctl,命令列在include/linux/sockios.h中。设备驱动程序可以用代码定义新的(私有)命令。其范围介于SIOCDEVPRIVATE和SIOCPRIVATE+15之间。例如,可以看看(虚拟)隧道设备使用的四个私有命令在include/linux/if_tuunel.h中是如何定义的。然而,使用私有的ioctl命令是被反对和不鼓励的。
各种协议也可以在SIOCPROTOPRIVATE到SIOCPOTOPRIVATE+15范围内定义私有命令。

Netlink

Netlink套接字在RFC 3549中描述的很清楚,代表用户空间与内核空间的IP网络配置之间的首选接口。Netlink也可作为内核内部以及多个用户空间进程之间的消息传输系统。
通过Netlink套接字,你可以使用标准套接字API打开或关闭套接字、使用套接字传输数据或者接收套接字数据。快速审视一下socket系统调用的原型:

	int socket(int domain, int type , int protocol)

有关TCP/IP套接字如何对此三个变量做初始化的细节,可以使用man socket命令。

如同其他任何套接字,当你打开一个Netlink套接字时,必须提供domain,type以及protocol参数。Netlink使用洗的PF_NETLINK协议族(域),只支持SOCK_DGRAM类型、而且定义了几种协议,每一种都用于网络协议栈的不同组件(或一组组件)。例如,NETLINK_ROUTE协议用于大多数网络功能,如路由和邻居协议,而NETLINK_FIRWALL用于防火墙(Netfilter)。Netlink的协议在include/linux/netlink.h中的NETLINK_XXX枚举表中。

使用Netlink套接字时,终端点通常是由打开此套接字的进程的ID(PID)标识,而特殊值0代表的就是内核。Netlink的功能之一是传送单播和多播消息:目的地终端点地址可以是一个PID、一个多播群组ID或两者的组合。内核定义Netlink多播群组的目的是传出特定种类事件的通知信息,而用户程序如果对这类通信信息感兴趣,可以向这些群组注册。这些群组列在include/linux/rtnetlink.h中的TRMGRP_XXX枚举列表中。而其中的两个RTMGRP_IPv4_ROUTE和RTMGRP_NETGH群组,分别用于通知有关路由表以及L3到L2的地址映射的改变。后面会详细分析。

另一个有趣的功能是传送正面和负面确认信息(acknowledment)。

Netlink相对于其他用户-内核接口(如ioctl)的优点之一,就是内核可以启动传输,而不是仅限于响应用户空间的请求而返回信息。

配置改变串行化

每次应用配置改变时,内核中内负责处理该事的例程都会取得一个信号量(rtnl_sem),以确保对存储网络配置内容的数据结构的访问具有互斥性。无论该配置的改变是通过ioctl还是Netlink而施加,都是如此。

总结

下一部分将围绕系统初始化进行学习和分析。这部分说明网络设备初始化以及与内核注册的方式和时机。会把重点放在PCI(Peripheral Component Interocnnect, 外设部件互联)设备上,因为PCI设备已逐普及,而且有特殊需求。

很多和NIC(networkl interoce card,网络接口卡)有关的任务都必须先完成,网络才能开启并运行。首先,关键内核组件必须予以初始化。其次,设备驱动程序必须对其负责的所有设备予以初始化和注册,然后分配一些资源(IRQ、I/O端口等)让内核能使用,以便与驱动程序通信。

区分两种注册是很重要的。首先,当设备被发现时就会向内核注册,成为通用设备。其次,NIC设备和网络协议栈注册时就成了网络设备。例如,PCI Ethernet卡会和PCI层注册成通用的PCI设备,同时和网络协议栈注册成Ethernet卡(在此设备取得eth0之类的名称)。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到