跳到主要内容

14.14 方法速查表(工具箱里的扳手和螺丝刀)

前面的旅程就真的到头了。

回想一下,这一章我们跨了很大的步子:

  • 我们从 Namespace 开始,建立了隔离的视角。
  • Cgroup 给资源套上了缰绳。
  • 跑到 蓝牙802.15.4NFC 的无线世界里看了看那些奇怪的协议。
  • 还钻进了内核的 通知链PCI 子系统。
  • 最后,我们看了一眼 PPPoEAndroid 这种把底层技术包装成上层平台的复杂案例。

还记得这一章开头我们说的吗?Linux 网络子系统是一个巨大的机器。这一章里的每一个小节——无论是多核的 RPS,还是 Android 的 HAL——其实都是在回答一个问题:当这个机器变得更复杂、更多核、更移动化时,我们要怎么驾驭它?

现在书已经翻到了最后一页。Linux 网络是一个浩瀚的海洋,新的特性还在不断地涌入内核主线。希望这本书不仅能让你学到几个 API 或几段代码,更能给你一张地图,让你在以后面对 ethtool 的输出或者内核崩溃日志时,知道该往哪里看,该问什么问题。

调试愉快!


为了方便你在后续的代码探险中查阅,我把这一章我们亲手摸过的那些核心内核方法和 API 整理成了这份速查表。你可以把它看作是工具箱里的说明书——不是为了从头读,而是为了在你写着写着突然想不起来「那个递增引用计数的函数叫什么来着?」的时候,能在这里立刻找到那把正确的扳手。

Namespace 管理

Namespace 是我们隔离世界的透镜。这一组方法负责创建、切换和销毁这些透镜。

void switch_task_namespaces(struct task_struct *p, struct nsproxy *new);

做什么:给指定的进程(p)换上一套新的「眼镜」(nsproxy)。 实质:这行代码执行后,该进程就彻底进入了新的命名空间视图——它看到的网络设备、主机名、PID 都会变成新 nsproxy 指向的那些。

struct nsproxy *create_nsproxy(void);

做什么:分配一个新的 nsproxy 结构体。 细节:不仅分配内存,还会把它的引用计数初始化为 1。这是创建新命名空间视图的第一步。

void free_nsproxy(struct nsproxy *ns);

做什么:释放指定的 nsproxy 对象占用的资源。 注意:通常在引用计数归零时调用。

struct nsproxy *task_nsproxy(struct task_struct *tsk);

做什么:获取指定进程当前佩戴的「眼镜」。 用途:当你有一个进程的 task_struct 却不知道它属于哪个命名空间时,用这个函数。

void get_nsproxy(struct nsproxy *ns);

做什么:给 nsproxy 的引用计数加 1。 目的:防止在你使用它的时候被别人释放掉。

void put_nsproxy(struct nsproxy *ns);

做什么:给 nsproxy 的引用计数减 1。 触发:如果减到 0,内核会调用 free_nsproxy() 彻底清理它。


网络命名空间

这是网络子系统里最核心的隔离机制。下面的每一个函数都在操作 struct net —— 那个代表了整个网络协议栈视图的对象。

struct net *dev_net(const struct net_device *dev);

做什么:回答「这个网卡属于哪个网络空间?」。 返回net_devicend_net 成员的值。

void dev_net_set(struct net_device *dev, struct net *net);

做什么:把指定的网卡移动到指定的网络空间里。 实质:修改 dev->nd_net 指针。

struct net *sock_net(const struct sock *sk);

做什么:回答「这个 Socket 属于哪个网络空间?」。 返回sock 结构里 sk_net 的值。

void sock_net_set(struct sock *sk, struct net *net);

做什么:给 Socket 指定一个归属的网络空间。 场景:Socket 创建时,必须把它挂到某个网络命名空间下,否则内核不知道该用哪张路由表。

int net_eq(const struct net *net1, const struct net *net2);

做什么:判断两个网络空间指针是不是同一个。 为什么需要它:因为有时候网络空间指针可能是 &init_net,快速判断是否相等能避免不必要的查找。

struct net *net_alloc(void);

做什么:分配一个新的 struct net 对象。 位置:它是 copy_net_ns() 的内部实现细节。

struct net *copy_net_ns(unsigned long flags, struct user_namespace *user_ns, struct net *old_net);

做什么:这是创建新网络空间的「总开关」。 逻辑

  1. 检查 flags 里有没有 CLONE_NEWNET
  2. 如果有,调用 net_alloc() 分配新对象,再用 setup_net() 初始化,最后挂到全局链表 net_namespace_list 上。
  3. 如果没有,返回 old_net(大家共享一个)。 注意:如果内核没开 CONFIG_NET_NS,这个函数会变成一个空壳,只检查 flags 并直接返回旧空间。

int setup_net(struct net *net, struct user_namespace *user_ns);

做什么:初始化新分配的网络空间对象。 细节:设置 user_ns,把引用计数置为 1,并执行所有注册到 pernet_operations 的初始化回调。

struct net *get_net_ns_by_pid(pid_t pid);

做什么:通过进程 PID 找到它所在的网络空间。 用途:当你只想操作「某个进程所在的网络空间」时,这步查找是必须的。

struct net *get_net_ns_by_fd(int fd);

做什么:通过文件描述符找到对应的网络空间。 原理:文件描述符可能指向一个代表网络空间的特殊文件(比如 /proc/[pid]/ns/net 的打开文件描述)。

void put_net(struct net *net);

做什么:减少网络空间的引用计数。 后果:如果计数归零,调用 __put_net() 回收资源。

struct net *get_net(struct net *net);

做什么:增加网络空间的引用计数,并返回指针。 模式:典型的「获取并使用」模式,防止对象在使用中消失。

int dev_change_net_namespace(struct net_device *dev, struct net *net, const char *pat);

做什么:把一个正在运行的网卡物理移动到另一个网络空间。 条件:调用者必须持有 rtnl 信号量(这是网络配置操作的锁)。 :如果网卡特性里设置了 NETIF_F_NETNS_LOCAL(意思是「我是本地设备,不能移动」),这个函数会直接返回 -EINVAL。有些虚拟设备是不允许跨命名空间迁移的。


网络命名空间子系统注册

当你写一个新的内核模块,并且想在每个网络空间创建时都做点什么事(比如初始化点数据结构),你需要用到这些注册函数。

int register_pernet_device(struct pernet_operations *ops);

做什么:注册一个设备级的网络空间回调。 特点:顺序靠后,用于那些依赖其他基础设备已经初始化好的模块。

void unregister_pernet_device(struct pernet_operations *ops);

做什么:注销上述回调。

int register_pernet_subsys(struct pernet_operations *ops);

做什么:注册一个子系统级的网络空间回调。 特点:顺序靠前,用于核心子系统(比如路由表、协议栈)。

void unregister_pernet_subsys(struct pernet_operations *ops);

做什么:注销上述回调。


UTS 命名空间

UTS 空间里装的是主机名和域名。这是最简单的命名空间之一。

struct new_utsname *utsname(void);

做什么:获取当前进程的 UTS 名称结构。 快捷方式:相当于拿 current->nsproxy->uts_ns->name

struct uts_namespace *clone_uts_ns(struct user_namespace *user_ns, struct uts_namespace *old_ns);

做什么:克隆一个 UTS 空间。 逻辑:调用 create_uts_ns() 分配,然后把旧空间里的主机名拷贝过去。

struct uts_namespace *copy_utsname(unsigned long flags, struct user_namespace *user_ns, struct uts_namespace *old_ns);

做什么:处理 clone 系统调用时的 UTS 空间创建。 逻辑:如果 flagsCLONE_NEWUTS,就调用 clone_uts_ns() 创建新的;否则返回旧的 old_ns


Proc 文件系统与 Inode

每个命名空间在 /proc 下都有一个唯一的身份证号。

int proc_alloc_inum(unsigned int *inum);

做什么:分配一个 proc inode 号码。 范围:在 0xf00000000xffffffff 之间。 用途:用来标识一个命名空间实例,确保在 /proc 里是唯一的。

struct pid_namespace *ns_of_pid(struct pid *pid);

做什么:返回指定 PID 所属的 PID 命名空间。 用途:搞清楚一个 PID 是在哪个「盒子」里生成的。


Cgroup 辅助

Cgroup 是资源限制的笼子。这里是当进程释放时的一些处理。

void cgroup_release_agent(struct work_struct *work);

做什么:当一个 Cgroup 被释放时,调用这个函数。 实质:它会在用户空间启动一个辅助进程(通过 call_usermodehelper()),用来处理清理工作。

int call_usermodehelper(char * path, char ** argv, char ** envp, int wait);

做什么:从内核空间启动一个用户空间程序。 能力:内核能让你跑个脚本,靠的就是这个。


蓝牙协议栈

蓝牙那一套复杂的层级,靠这些方法粘在一起。

int bacmp(bdaddr_t *ba1, bdaddr_t *ba2);

做什么:比较两个蓝牙地址。 返回:0 表示相等。

void bacpy(bdaddr_t *dst, bdaddr_t *src);

做什么:把蓝牙地址从 src 拷贝到 dst

int hci_send_frame(struct sk_buff *skb);

做什么:发送 HCI 层的数据包(命令或数据)。 地位:蓝牙传输的「总闸门」。

int hci_register_dev(struct hci_dev *hdev);

做什么:向内核注册一个 HCI 设备。 :如果 hdev 里的 open()close() 回调没定义,直接返回 -EINVAL。它还会设置 HCI_SETUP 标志,并创建对应的 sysfs 条目。

void hci_unregister_dev(struct hci_dev *hdev);

做什么:注销 HCI 设备。 动作:设置 HCI_UNREGISTER 标志,删掉 sysfs 条目。

int hci_event_packet(struct hci_dev *hdev, struct sk_buff *skb);

做什么:处理从 HCI 层收上来的事件包。 触发:由 hci_rx_work() 调用,是事件处理器的入口。


6LoWPAN

在低功耗无线网络上跑 IPv6,靠的是这个适配层。

int lowpan_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev);

做什么:6LoWPAN 的主接收处理函数。 特征:这种包的以太网类型是 0x00F6


PCI 子系统

绝大多数网卡都是 PCI 设备。这些是驱动与 PCI 总线交互的基础。

void pci_unregister_driver(struct pci_driver *dev);

做什么:注销 PCI 驱动。 位置:通常写在模块的 exit 函数里。

int pci_enable_device(struct pci_dev *dev);

做什么:唤醒并初始化 PCI 设备。 时机:在驱动真正开始使用设备前,必须先调这个。

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);

做什么:注册中断服务程序。 绑定:把你的处理函数 handler 挂到中断线 irq 上。

void free_irq(unsigned int irq, void *dev_id);

做什么:释放中断线。 对应request_irq 的反操作。


NFC (Near Field Communication)

NFC 子系统的初始化和设备注册。

int nfc_init(void);

做什么:NFC 子系统的总初始化。 动作:注册 Generic Netlink 家族、初始化 Raw Socket、LLCP Socket 和 AF_NFC 协议。

int nfc_register_device(struct nfc_dev *dev);

做什么:向 NFC 核心层注册一个 NFC 设备。

int nfc_hci_register_device(struct nfc_hci_dev *hdev);

做什么:注册一个 NFC HCI 设备。

int nci_register_device(struct nci_dev *ndev);

做什么:注册一个 NFC NCI 设备。


PPPoE (Point-to-Point Protocol over Ethernet)

这是 DSL 连接背后的协议。下面是它的核心操作。

static int __init pppoe_init(void);

做什么:PPPoE 层的入口初始化。 动作:注册协议处理器、Socket 类型、网络通知链以及 /proc 条目。

struct pppoe_hdr *pppoe_hdr(const struct sk_buff *skb);

做什么:从一个 Socket 缓冲区里提取 PPPoE 头部。

static int pppoe_create(struct net *net, struct socket *sock);

做什么:创建一个 PPPoE Socket。 错误:如果 sk_alloc() 分配失败,返回 -ENOMEM

int __set_item(struct pppoe_net *pn, struct pppox_sock *po);

做什么:把一个 PPPoE Socket 插入哈希表。 哈希键:根据 Session ID 和对方 MAC 地址算出来的。

void delete_item(struct pppoe_net *pn, __be16 sid, char *addr, int ifindex);

做什么:从哈希表里删掉一个 PPPoE Socket。 定位:靠 Session ID、MAC 地址和网卡索引三者共同定位。

bool stage_session(__be16 sid);

做什么:判断 Session ID 是否有效(非 0)。


通知链

这是内核里「大喊一声让所有人都听见」的机制。我们在前面看到网络设备状态变化时全靠它。

int notifier_chain_register(struct notifier_block **nl, struct notifier_block *n);

做什么:把一个回调块 n 挂到链表 nl 上。 注意:这是底层原语,实际使用时通常有封装函数。

int notifier_chain_unregister(struct notifier_block **nl, struct notifier_block *n);

做什么:从链表上摘掉一个回调块。

int register_netdevice_notifier(struct notifier_block *nb);

做什么:注册到网络设备通知链(netdev_chain)。 封装:里面调的是 raw_notifier_chain_register()

int unregister_netdevice_notifier(struct notifier_block *nb);

做什么:从网络设备通知链注销。

int register_inet6addr_notifier(struct notifier_block *nb);

做什么:注册到 IPv6 地址通知链。

int unregister_inet6addr_notifier(struct notifier_block *nb);

做什么:从 IPv6 地址通知链注销。

int register_netevent_notifier(struct notifier_block *nb);

做什么:注册到网络事件通知链。

int unregister_netevent_notifier(struct notifier_block *nb);

做什么:从网络事件通知链注销。

int __kprobes notifier_call_chain(struct notifier_block **nl, unsigned long val, void *v, int nr_to_call, int nr_calls);

做什么:触发通知链,让上面挂着的所有回调函数跑一遍。 底层:这是引擎,通常不直接用。

int call_netdevice_notifiers(unsigned long val, struct net_device *dev);

做什么:专门给网络设备发通知。 动作:调用 raw_notifier_call_chain()

int blocking_notifier_call_chain(struct blocking_notifier_head *nh, unsigned long val, void *v);

做什么:带锁的通知调用。 场景:用在可能睡眠的上下文中。

int __atomic_notifier_call_chain(struct atomic_notifier_head *nh,unsigned long val, void *v, int nr_to_call, int nr_calls);

做什么:原子上下文的通知调用(不可睡眠)。


最后,回到那个关于「驾驭复杂性」的问题

看这份列表,你会发现所有的 API 都在围绕几个核心动作:注册注销关联查找。内核网络子系统再庞大,本质上也就是这些动作在无数的层级和对象之间反复上演。

当你真的理解了这一章里的这些工具——知道它们什么时候被调用,为什么要这样设计——你就不再是单纯地在「用」Linux 网络了,你是在「懂」它。

祝你的下一次调试不再迷茫。