概论

内核通过各种不同的接口把内部信息输出到用户空间。

除了系统调用外,还有三个特殊接口(其中两个是虚拟文件系统)

procfs (/proc 文件系统)

  • 虚拟文件系统,通过 mount 在 /proc,允许内核以文件的形式向用户控件输出内部信息

sysctl (/proc/sys 目录)

  • 此接口允许用户空间读取或修改内核变量的值

  • 用户空间可以使用两种方式访问 sysctl 输出的变量

    • sysctl 系统调用 (man sysctl)
    • procfs, 当内核支持 procfs 时,会在 /proc 中添加一个特殊目录(/proc/sys),为每个由 sysctl 所输出的内核变量引入一个文件
    • sysctl 命令可用于配置由 sysctl 接口所输出的变量

sysfs (/sys 文件系统)

  • 新的文件系统,以非常干净而有组织的方式输出很多信息

你还可以使用以下接口向内核发送命令,既可以用于配置某些内容,也可以用于转储其他内容的配置。

ioctl 系统调用

  • ioctl(输入/输出控制)系统调用操作的对象是一个文件

Netlink 套接字 (socket)

  • 网络应用程序与内核通信时的首选机制

procfs 与 sysctl

相同点:

  • 输出内核内部信息

不同点:

  • procfs

    • 主要输出只读数据
    • 复杂的数据结构且需要特殊格式时,如缓存和统计数据
  • sysctl

    • 大多数都可写入,但只有 superuser 能写入
    • 一个简单的内核变量或数据结构相关联的一些文件

procfs

  • 大多数网络功能在其初始化时都会在 /proc 中注册一个或多个文件。

  • 网络代码所注册的文件位于 /proc/net

1
2
3
4
5
6
7
8
9
// 创建 /proc 中的目录
proc_mkdir

// 注册 /proc/net 中的文件
proc_net_fops_create
create_proc_entry

proc_net_remove
remove_proc_entry

示例:ARP 协议如何在 /proc/net 中注册其 arp 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 __init arp_proc_init(void)
{
if (!proc_net_fops_create("arp", S_IRUGO, &arp_seq_fops))
return -ENOMEM;
return 0;
}

当用户读取该文件时,使用 file_operations 数据结构,允许 procfs 返回相当多的数据给用户。

arp_seq_open 会做另一次重要的初始化:注册一个函数指针数组

1
2
3
4
5
6
7
8
9
10
11
12
13
static struct seq_operations arp_seq_ops = {
.start = clip_seq_start,
.next = neigh_seq_next,
.stop = neigh_seq_stop,
.show = clip_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/net/ipv4 中可以找到与 IPv4 相关的文件

/proc/sys 中的文件和目录都是以 ctl_table 结构定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* A sysctl table is an array of struct ctl_table: */
struct ctl_table
{
int ctl_name; /* Binary ID */
// 在 /proc/sys 中所用的文件名
const char *procname; /* Text ID for /proc/sys, or zero */
void *data;
// 输出的内核变量的尺寸大小
int maxlen;
// 分派给 /proc/sys 中相关联的文件或目录的访问权限
mode_t mode;
// 用于建立目录与文件之间的父子关系
ctl_table *child;
// 在 /proc/sys 中读取或写入一个文件时,完成读取或写入操作的函数
// 所有与文件相关联的 ctl_instances 都必须由 proc_handler 初始化
// 内核会给目录分派一个默认值
proc_handler *proc_handler; /* Callback for text formatting */
// 完成数据的额外格式化工作的函数。
// 当使用 sysctl 系统调用访问 /proc/sys 中的文件时调用
ctl_handler *strategy; /* Callback function for all r/w */
struct proc_dir_entry *de; /* /proc control block */
// 通常用于定义变量的最小值和最大值
void *extra1;
void *extra2;
};

ctl_table 初始化的实例

/proc/sys/net/ipv4/conf/default/forwarding 文件所用的 ctl_table 实体的初始化定义在 net/ipv4/devinet.c

1
2
3
4
5
6
7
8
{
.ctl_name = NET_IPV4_CONF_FORWARDING,
.procname = "forwarding",
.data = &ipv4_devconf.forwarding,
.maxlen = sizeof(int),
.mode = 0644,
.proc_handler = &devinet_sysctl_forward,
},
  • r:4, w:2, x:1

kernel/sysctl.c 中一个目录的声明实例:

1
2
3
4
5
6
{
.ctl_name = CTL_NET,
.procname = "net",
.mode = 0555,
.child = net_table,
},
  • 定义了 /proc/sys/net 目录
  • 不需要 proc_handler
  • child 指针,指向另一个 ctl_table 实体,ctl_table 实体列表的头元素

在 /proc/sys 中注册文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct ctl_table_header *register_sysctl_table(ctl_table * table, 
int insert_at_head)
{
struct ctl_table_header *tmp;
tmp = kmalloc(sizeof(struct ctl_table_header), GFP_KERNEL);
if (!tmp)
return NULL;
tmp->ctl_table = table;
INIT_LIST_HEAD(&tmp->ctl_entry);
tmp->used = 0;
tmp->unregistering = NULL;
spin_lock(&sysctl_lock);
if (insert_at_head)
list_add(&tmp->ctl_entry, &root_table_header.ctl_entry);
else
list_add_tail(&tmp->ctl_entry, &root_table_header.ctl_entry);
spin_unlock(&sysctl_lock);
#ifdef CONFIG_PROC_SYSCTL
register_proc_table(table, proc_sys_root, tmp);
#endif
return tmp;
}

void unregister_sysctl_table(struct ctl_table_header * header)
{
might_sleep();
spin_lock(&sysctl_lock);
start_unregistering(header);
#ifdef CONFIG_PROC_SYSCTL
unregister_proc_table(header->ctl_table, proc_sys_root);
#endif
spin_unlock(&sysctl_lock);
kfree(header);
}

示例:文件 logging_level 的定义以及如何放置到 /proc/sys/dev/scsi 目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 },
{ }
};

static ctl_table scsi_root_table[] = {
{ .ctl_name = CTL_DEV,
.procname = "dev",
.mode = 0555,
.child = scsi_dir_table },
{ }
};

static struct ctl_table_header *scsi_table_header;

int __init scsi_init_sysctl(void)
{
scsi_table_header = register_sysctl_table(scsi_root_table, 1);
if (!scsi_table_header)
return -ENOMEM;
return 0;
}

核心网络文件和目录

  • 每个目录以及目录中的每个文件,都是一个 ctl_table 实体

图 3-3 中的三个方块显示了 ctl_table 初始化的三个实例:

  • netdev_max_backlog 文件被分派了一个 proc_handler 例程,但没有 strategy。因为 netdev_max_backlog 是一个整数,来自于用户的输入由 proc_dointvec 读取。

  • min_delay 文件被分派了 proc_handler 和 strategy 。因为内核变量 ip_rt_min_delag 已 jiffies 表示,但是用户的输入和输出都是以秒来表示,这两个例程可以完成秒转换为 jiffies。

  • ip_local_port_range 文件允许用户配置一个范围,定义两个值,所选的 strategy 和 proc_handler 例程必须能够管理一个整数值的数组。extra1 和 extra2 表示这个范围。

ioctl

ifconfig 命令使用 ioctl 与内核通信。

当系统管理员输入 ifconfig eth0 mtu 1250 命令改变 eth0 的 MTU 时,

  • ifconfig 会打开一个套接字,用从系统管理员那里接受的信息初始化一个本地数据结构
  • 然后以 ioctl 调用传送给内核
1
2
3
4
struct ifreq data;
fd = socket(PF_INET, SOCK_DGRAM, 0);
< ... 对"data"初始化 ... >
err = ioctl(fd, SIOCSIFMTU, &data);

图3-4 显示了网络代码最常用的 ioctl 命令如何由 sock_ioctl 分派,并且路由至正确的函数处理例程。

SIOC ADD RT : 把一条路径新增至路由表的命令 (SIOCADDRT)

  • ADD: 添加

  • RT: 路由

  • G: 取得

  • S: 设置

SIOCGIFADDR, SIOCSIFADDR 两个命令为接口新增或删除 IP 地址。

SIOCSIFMTU 是设定(S)接口(IF)的最大传输单元(MTU),由 dev_ioctl 所做。

网络用的 ioctl 命令在 include/linux/sockios.h 中。

设备驱动程序可以定义新的(私有)命令,其范围介于 SIOCDEVPRIVATESIOCDEVPRIVATE+15之间。

include/linux/if_tunnel.h 中定义的(虚拟)隧道设备使用的四个私有命令。

1
2
3
4
#define SIOCGETTUNNEL   (SIOCDEVPRIVATE + 0)
#define SIOCADDTUNNEL (SIOCDEVPRIVATE + 1)
#define SIOCDELTUNNEL (SIOCDEVPRIVATE + 2)
#define SIOCCHGTUNNEL (SIOCDEVPRIVATE + 3)
  • 用户空间与内核的 IP 网络配置之间的首选接口

  • 也可作为内核内部以及多个用户空间进程之间的消息传输系统

通过 Netlink 套接字,可以使用标准套接字 API

1
2
3
#include <sys/socket.h>

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

Netlink 使用新的 PF_NETLINK 协议族,只支持 SOCK_DGRAM 类型,在include/linux/netlink.h定义了几种协议:

  • NETLINK_ROUTE: 大多数网络功能,如路由和邻居协议

  • NETLINK_FIREWALL: 防火墙 (Netfilter)

使用 Netlink 套接字时,endpoints 通常由打开此套接字的进程ID (PID) 标识,0 表示内核。

可传送单播和多播消息:目的地可以使一个 PID,一个多播群组 ID 或两者组合。

Netlink 多播群组:

  • 传出特定种类事件的通知信息,用户程序如果对这类通知信息感兴趣,可以向这些群组注册。
  • include/linux/rtnetlink.hPTMGRP_XXX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef __KERNEL__
/* RTnetlink multicast groups - backwards compatibility for userspace */
#define RTMGRP_LINK 1
#define RTMGRP_NOTIFY 2
// L3 到 L2 的地址映射的改变
#define RTMGRP_NEIGH 4
#define RTMGRP_TC 8

#define RTMGRP_IPV4_IFADDR 0x10
#define RTMGRP_IPV4_MROUTE 0x20
// 通知有关路由表
#define RTMGRP_IPV4_ROUTE 0x40
#define RTMGRP_IPV4_RULE 0x80

#define RTMGRP_IPV6_IFADDR 0x100
#define RTMGRP_IPV6_MROUTE 0x200
#define RTMGRP_IPV6_ROUTE 0x400
#define RTMGRP_IPV6_IFINFO 0x800

#define RTMGRP_DECnet_IFADDR 0x1000
#define RTMGRP_DECnet_ROUTE 0x4000

#define RTMGRP_IPV6_PREFIX 0x20000
#endif

Netlink 的优点之一,内核可以启动传输,而不只是仅限于响应用户控件请求而返回数据。

配置改变串行化

每当应用配置改变时,内核中负责处理此事的例程都会取得一个信号量(rtnl_sem),以确保对存储网络配置内容的数据结构的访问具有互斥性。