14.14 方法速查表(工具箱里的扳手和螺丝刀)
前面的旅程就真的到头了。
回想一下,这一章我们跨了很大的步子:
- 我们从 Namespace 开始,建立了隔离的视角。
- 用 Cgroup 给资源套上了缰绳。
- 跑到 蓝牙、802.15.4 和 NFC 的无线世界里看了看那些奇怪的协议。
- 还钻进了内核的 通知链 和 PCI 子系统。
- 最后,我们看了一眼 PPPoE 和 Android 这种把底层技术包装成上层平台的复杂案例。
还记得这一章开头我们说的吗?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_device 里 nd_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);
做什么:这是创建新网络空间的「总开关」。 逻辑:
- 检查
flags里有没有CLONE_NEWNET。 - 如果有,调用
net_alloc()分配新对象,再用setup_net()初始化,最后挂到全局链表net_namespace_list上。 - 如果没有,返回
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 空间创建。
逻辑:如果 flags 有 CLONE_NEWUTS,就调用 clone_uts_ns() 创建新的;否则返回旧的 old_ns。
Proc 文件系统与 Inode
每个命名空间在 /proc 下都有一个唯一的身份证号。
int proc_alloc_inum(unsigned int *inum);
做什么:分配一个 proc inode 号码。
范围:在 0xf0000000 到 0xffffffff 之间。
用途:用来标识一个命名空间实例,确保在 /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 网络了,你是在「懂」它。
祝你的下一次调试不再迷茫。