netlink_setsockopt() 为何未触发 wake_up_interruptible()?
结论
根据给出的内核代码,当 optlen 为 1 时,optlen >= sizeof(int) 不成立,get_user() 不会执行,内核局部变量 val 保持初始值 0。代码随后仍可进入 case NETLINK_NO_ENOBUFS,但会执行其中的 else 分支,不会调用 wake_up_interruptible()。
用户态示例的传参类型也有问题:val 是 char *,因此 &val 指向保存指针值的变量,sizeof(val) 得到的是指针大小。应使用整数选项值,传入该整数的地址和大小。
为什么长度为 1 时跳过读取
内核先将变量初始化为零:
unsigned int val = 0;
随后执行条件判断:
if (optname != NETLINK_RX_RING &&
optname != NETLINK_TX_RING &&
optlen >= sizeof(int) &&
get_user(val, (unsigned int __user *)optval))
return -EFAULT;
C 的 && 从左向右求值,遇到不成立的条件时会短路。对于 NETLINK_NO_ENOBUFS,前两个条件成立;当 optlen 为 1 且小于 sizeof(int) 时,第三个条件不成立,后面的 get_user() 就不会执行。
长度条件不成立只会让整个 if 条件为假,不会直接退出函数。假设前面的调用路径正常,代码仍会继续执行 switch。
进入对应的 case 后,val 仍为零,因此执行:
nlk->flags &= ~NETLINK_RECV_NO_ENOBUFS;
按照这段代码,该分支随后设置 err = 0。因此,即使没有触发目标调用,setsockopt() 也可能调用成功。
为什么 sizeof(val) 看起来有效
用户态代码声明的是一个指针:
char *val = "A";
这几个表达式的含义不同:
val:指向字符串"A"的指针。&val:指向这个指针变量的地址。sizeof(val):指针类型的大小,与字符串长度无关。
当 sizeof(val) >= sizeof(int) 时,长度条件成立,内核会尝试执行:
get_user(val, (unsigned int __user *)optval)
get_user() 成功时返回 0,并将读取到的值写入内核局部变量 val;读取失败时,这里的条件才会成立并返回 -EFAULT。
由于传入的是 &val,内核读取的是指针变量所占内存中一个 unsigned int 大小的数据,而不是字符串中的字符 A。如果读出的整数恰好非零,代码就会进入 if (val) 并调用 wake_up_interruptible()。
这可以解释观察到的现象,但不能保证这种写法总能触发该分支。结果取决于指针值的内存表示和实际读取到的内容。
正确传递选项值
假设 fd 是有效的 netlink socket 描述符,可以用整数表示启用该选项:
#include <sys/socket.h>
#include <linux/netlink.h>
#include <stdio.h>
/* fd 已指向一个有效的 netlink socket。 */
int opt = 1;
if (setsockopt(fd, SOL_NETLINK, NETLINK_NO_ENOBUFS,
&opt, sizeof(opt)) == -1) {
perror("setsockopt");
}
这样,内核读取到的是整数 1。在所示实现中,只要读取成功并进入该 case,就会执行设置标志、清除 NETLINK_CONGESTED 和调用 wake_up_interruptible() 的分支。
若将 opt 设为 0,则执行清除 NETLINK_RECV_NO_ENOBUFS 标志的分支。
排查时需要区分的情况
先检查 setsockopt() 的返回值,失败时再通过 errno 判断原因。原示例没有检查返回值,因此无法仅凭“没有触发目标函数”确定系统调用是否报错。
以上分析以题目给出的内核代码为依据,并假设文件描述符有效、调用能够到达 netlink_setsockopt()。如果 dangling_fd 实际上已经失效,还需要先排查描述符状态。
另外,调用 wake_up_interruptible() 后是否实际唤醒某个任务,还取决于等待队列中是否存在可被唤醒的任务。
备注:内容仅供参考。
