一、SYSCALL_DEFINE3与系统调用
在Linux操作系统中,为了从用户空间跳转到内核空间执行特定的内核级操作,使用了一种机制叫做"系统调用"(System Call)。系统调用是操作系统提供给程序员访问和使用内核功能的接口。例如,要在文件系统中创建新文件、发送网络数据或分配内存等,都需要通过系统调用来完成。
SYSCALL_DEFINE3是一个Linux内核中用来定义接收三个参数的系统调用的宏。让我们深入理解一下这个过程。
宏 SYSCALL_DEFINE3
1. 名称: SYSCALL_DEFINE3是一个合成了名称中参数数量(在这个例子中是3)的宏,表示这个宏会处理一个有三个参数的系统调用。
2. 参数:
- socket: 这是系统调用的名字。在用户空间,例如在C语言中,我们可能使用类似`socket(…)`这样的调用。
- int, family: 这是系统调用的第一个参数,它指定了socket的协议族。
- int, type: 这是系统调用的第二个参数,它指定socket的类型。
- int, protocol: 这是系统调用的第三个参数,它指定了在给定的协议族和类型下使用哪个协议。
系统调用的工作流程
1. 用户空间调用系统调用:
程序员在用户空间程序(如C程序)中写下了一个系统调用,例如`socket(AF_INET, SOCK_STREAM, 0)`。
2. 转换为内核空间:
当上述调用执行时,CPU切换到内核模式,这个过程通常涉及到生成一个软件中断(如x86架构的`int 0x80`指令,或者其他架构的类似机制),或者使用特定的系统调用指令(如x86-64架构的`syscall`)。
3. 系统调用分派:
内核中有一个预先设置好的表(系统调用表),其中包括了所有系统调用对应的函数指针。执行软件中断时,会使用寄存器中的值(通常是一个系统调用号)来确定需要调用的具体函数。在x86-64架构上,这个系统调用号会被放在`RAX`寄存器中。然后系统调用表会返回对应的函数(例如`sys_socket`)。
4. 执行内核级函数:
在这个例子中,内核会执行`__sys_socket`函数,这是内核中实现创建socket实际操作的私有函数。`__sys_scket`负责分配和设置必要的数据结构,以创建和配置一个新的socket。
5. 返回用户空间:
一旦`__sys_socket`完成了它的工作,系统调用会返回到用户空间程序,通常携带一个值——file descriptor(文件描述符),这个值是新创建的socket的唯一标识。如系统调用执行成功,返回的是一个非负整数的文件描述符;如执行失败,就返回一个错误码(一般是-1)。
结论
通过使用宏如`SYSCALL_DEFINE3`,Linux内核的维护者可以轻松定义需要任何数量参数的系统调用,同时保持代码简洁和一致性。借助此宏能够将系统调用名称与实现细节脱钩,这样在系统调用实现或调度逻辑发生改变时,不需要对每个系统调用的定义都进行修改。
二、SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
这行代码定义了一个Linux内核系统调用`socket`,其接收三个整型参数:`family`,`type`,和`protocol`。这个系统调用用来创建一个新的socket。
来逐个解释这些参数:
- family:指定了socket所使用的协议族。比如,`AF_INET`表示IPv4协议族,`AF_INET6`表示IPv6协议族,等等。
- type:指定了socket的类型,也就是通信的语义。例如,`SOCK_STREAM`表示提供顺序、可靠、双向、基于连接的字节流;`SOCK_DGRAM`表示提供数据报(一个小块的信息),它们可能会丢失或者重复,并可能不会按顺序到达。
- protocol:通常为零,让系统自动选择`family`和`type`组合的默认协议。例如,当使用`AF_INET`和`SOCK_STREAM`,默认协议是`IPPROTO_TCP`,即TCP协议。
函数名`SYSCALL_DEFINE3`是一个宏,用于定义接收三个参数的系统调用。这个宏展开后,会生成系统调用的实际实现,这里它定义了`socket`系统调用。
实现部分:`__sys_socket(family, type, protocol)`;
这是一个内核私有的函数,负责实现创建socket的逻辑。它简单地将任务委托给`__sys_socket`函数处理。这个函数会处理实际的socket创建工作,并返回一个文件描述符(file descriptor),这个文件描述符代表了新创建的socket。如果操作成功,这个文件描述符是一个非负整数;如果操作失败,则返回-1,并且设置相应的错误码。
三、int __sys_socket(int family, int type, int protocol)
int __sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
int flags;
/* Check the SOCK_* constants for consistency. */
BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return retval;
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
这段代码是一个内核函数的示例,用于创建一个新的socket。它包含了在进行网络编程时创建socket的一些基本步骤和错误检查。以下是代码的解释:
1. 函数声明: int __sys_socket(int family, int type, int protocol) 定义了一个叫做 __sys_socket 的函数,接受三个参数:`family` 表示socket的地址族(如AF_INET),`type` 表示socket的类型(如SOCK_STREAM或SOCK_DGRAM),`protocol` 表示使用的协议(如IPPROTO_TCP)。
2. 函数内的局部变量声明:
- int retval; 用来存储函数调用的返回值。
- struct socket *sock; 定义一个指向 socket 结构体的指针变量。
- int flags; 用于存储类型中的标志位。
3. 常量检查: BUILD_BUG_ON 宏用来在编译时检查某些情况是否真实,如果条件为真,编译将失败。此处检查了几个有关Socket的常量设置是否一致,以确保在编译时没有逻辑错误。
4. 标志处理:
- 该函数初期根据 type 参数中的标志位获取 flags。
- 检查 flags 是否包含除了 SOCK_CLOEXEC 和 SOCK_NONBLOCK 外的其他标志,如果包含则返回错误值 -EINVAL(表示参数无效)。
- 然后,剔除 type 中的标志位,将其仅留下socket类型标志。
5. 非阻塞标志转换:
- 如果 SOCK_NONBLOCK 和 O_NONBLOCK 不相同,但 flags 中的 SOCK_NONBLOCK 被设置了,那么就将 flags 中的 SOCK_NONBLOCK 转换为 O_NONBLOCK。
6. 创建socket:
- 使用 sock_create 函数尝试创建一个socket,传入`family`, type, protocol 和一个指向 socket 结构体指针的地址。
- 如果创建失败(即返回值小于0),则直接返回错误的返回值。
7. 返回文件描述符:
- 如果socket创建成功,会使用 sock_map_fd 函数将socket映射到一个文件描述符,同时将 flags 中的 O_CLOEXEC 和 O_NONBLOCK 标志传递给该函数。
- 函数最终返回一个文件描述符,该描述符可以用于后续的socket操作。
四、int __sock_create(struct net *net, int family, int type, int protocol, struct socket **res, int kern)
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
// 确认传入的协议族 `family` 是否在有效范围中
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
// 确认传入的套接字类型 `type` 是否在有效范围中
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;
// 废弃的兼容性代码。如使用了SOCK_PACKET,将其转换为PF_PACKET
if (family == PF_INET && type == SOCK_PACKET) {
pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",
current->comm);
family = PF_PACKET;
}
// 安全模块检查创建套接字请求是否允许
err = security_socket_create(family, type, protocol, kern);
if (err)
return err;
// 为套接字分配内存
sock = sock_alloc();
if (!sock) {
net_warn_ratelimited("socket: no more sockets\n");
return -ENFILE; // 返回文件句柄不足的错误
}
sock->type = type;
// 尝试加载协议族相关的模块(如果需要)
if (rcu_access_pointer(net_families[family]) == NULL)
request_module("net-pf-%d", family); // 请求加载协议族对应的模块(如果需要)
rcu_read_lock();
pf = rcu_dereference(net_families[family]);
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;
// 尝试获取协议族模块的引用计数,如果获取失败则释放资源
if (!try_module_get(pf->owner))
goto out_release;
// 如果获取成功,则调用该协议族的create方法来完成套接字的创建
rcu_read_unlock();
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
// 如果套接字创建成功,增加套接字操作模块的引用计数,这样避免在使用期间模块被卸载
if (!try_module_get(sock->ops->owner))
goto out_module_busy;
// 套接字创建完毕后,减少协议族模块的引用计数
module_put(pf->owner);
// 安全模块对新创建的套接字进行后处理
err = security_socket_post_create(sock, family, type, protocol, kern);
if (err)
goto out_sock_release;
// 创建成功,将结果赋值给输出参数 `res`
*res = sock;
return 0;
out_module_busy:
err = -EAFNOSUPPORT;
out_module_put:
// 如果模块忙碌,清理并释放协议族模块的引用计数
sock->ops = NULL;
module_put(pf->owner);
out_sock_release:
// 释放创建的套接字
sock_release(sock);
return err;
out_release:
// 释放读锁并释放套接字
rcu_read_unlock();
goto out_sock_release;
}
EXPORT_SYMBOL(__sock_create);
这段代码是一个内核函数 __sock_create
,它用于在Linux内核中创建一个新的套接字。
这个函数定义了创建一个新套接字的流程,在网络编程中执行一个非常重要的角色。它主要执行以下步骤:
1. 检查是否传入了有效的 family(协议族,如IPv4或IPv6)和 type(套接字类型,如流或数据报)参数。如果参数超出了预定义的范围,函数会返回对应的错误码。
2. 旧版兼容性处理,如果使用的是PF_INET和SOCK_PACKET,就发出警告并将family改成PF_PACKET。
3. 调用安全模块中的 security_socket_create 函数来执行额外的安全检查。如果安全检查失败,它将返回一个错误码。
4. 尝试为新的套接字分配内存。如果没有足够的内存可供套接字使用,则返回一个错误码。
5. 如果上文中的某些协议家族尚不存在于 net_families 数组中(即该协议没有注册),则尝试动态加载对应的协议家族模块。
6. 获取对应的协议家族结构 pf,如果 pf 为NULL或无法获取其模块的引用,将释放资源并返回错误。
7. 如果协议模块的引用成功获取,则调用协议的 ->create 函数创建套接字。如果创建失败,释放相关资源并返回错误。
8. 协议族模块的作用已完成,模块引用计数减一。
9. 使用 security_socket_post_create 函数执行套接字创建后的安全检查,如果检查失败,进行资源清理并返回错误。
10. 如果一切顺利,将 sock 指针赋给输出参数 res,表示套接字创建成功,函数返回0。
异常处理标签 out_module_busy、`out_module_put`、`out_sock_release` 是用于错误发生时的资源清理工作。例如,释放套接字、减少模块引用计数等。
最后,`EXPORT_SYMBOL` 宏将 __sock_create 函数导出,这允许其他内核模块调用这个函数。
五、rcu_read_lock和rcu_read_unlock
rcu_read_lock 和 rcu_read_unlock 是在Linux内核中使用的一对函数,它们被用来保护使用读-复制更新(Read-Copy Update, RCU)机制的数据结构。这个机制允许多个读者并行访问数据结构,而写者则能够在不阻塞读者的情况下修改数据结构。
rcu_read_lock 的作用:
当一个内核线程调用 rcu_read_lock,它标记了一个临界区开始,这个临界区内的读者可以安全地读取RCU保护的数据结构,即使有其他线程正在更改这些数据结构。`rcu_read_lock` 通常不会阻塞调用它的线程,并且它并不保护数据结构免受更改,而是确保在临界区内读取的数据结构在读取时是一致的。在这个临界区内,任何持续对数据结构的更新操作都不会被看到,这保证了数据结构的读取视图是一致的。
rcu_read_unlock 的作用:
rcu_read_unlock 标记了RCU读临界区的结束。它告诉内核,线程已经完成对RCU保护数据的读取,在此之后对数据结构的更改可能对该线程可见。在`rcu_read_unlock` 调用之后,线程不再保证能访问之前RCU保护的数据结构的一致视图。
使用这对函数可以提高并发性能,因为它们消除了传统锁机制造成的读者与写者之间的竞争,允许大量并行的读取操作,而不会与可能并发发生的写入操作冲突。然而,写入操作需要使用专门的RCU API来确保数据结构的更改能够被安全地发布给未来的读者。
六、static int sock_map_fd(struct socket *sock, int flags)
这个函数`sock_map_fd`是一个静态函数,它的主要作用是把一个已创建的套接字(struct socket
)映射到一个文件描述符上。以下是该函数的步骤和解释:
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile; // 定义一个用于文件描述符的指针变量
int fd = get_unused_fd_flags(flags); // 获取一个未使用的文件描述符
if (unlikely(fd < 0)) { // 如果获取失败(例如因为没有可用描述符)
sock_release(sock); // 释放之前分配的socket资源
return fd; // 返回错误码(负值)
}
newfile = sock_alloc_file(sock, flags, NULL); // 为socket分配一个file结构
if (likely(!IS_ERR(newfile))) { // 如果分配成功
fd_install(fd, newfile); // 把这个新的file结构和之前获得的描述符fd关联起来
return fd; // 返回这个新的文件描述符
}
put_unused_fd(fd); // 如果分配失败,则释放之前获得的文件描述符
return PTR_ERR(newfile); // 返回对应的错误码
}
函数首先通过调用`get_unused_fd_flags(flags)`获得一个未被使用的文件描述符(fd)。如果无法获得有效的文件描述符(例如所有的描述符都已被占用),该函数使用`sock_release(sock)`来释放套接字资源,并且返回一个错误码。
如果成功获取到一个有效的文件描述符,函数接着会尝试使用`sock_alloc_file(sock, flags, NULL)`来为套接字分配一个对应的`file`结构体。如果这个分配过程成功则没有出错,它会将这个新的`file`结构体和之前获得的文件描述符关联起来,使用`fd_install(fd, newfile)`完成这个操作。
如果`file`结构体分配失败(例如内存不足),函数会释放之前获得的文件描述符`fd`,使用`put_unused_fd(fd)`,并返回分配`file`时的错误码。
最后,如果所有操作都成功了,这个函数会返回一个正整数,它是新创建的可以用于操作套接字的文件描述符。如果操作失败,它会返回一个错误码用来表示具体何种错误发生。