【C语言】SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)

2024-02-09 11:12

本文主要是介绍【C语言】SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、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_PACKETif (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`时的错误码。
最后,如果所有操作都成功了,这个函数会返回一个正整数,它是新创建的可以用于操作套接字的文件描述符。如果操作失败,它会返回一个错误码用来表示具体何种错误发生。

这篇关于【C语言】SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/694059

相关文章

C语言中联合体union的使用

本文编辑整理自: http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=179471 一、前言 “联合体”(union)与“结构体”(struct)有一些相似之处。但两者有本质上的不同。在结构体中,各成员有各自的内存空间, 一个结构变量的总长度是各成员长度之和。而在“联合”中,各成员共享一段内存空间, 一个联合变量

大语言模型(LLMs)能够进行推理和规划吗?

大语言模型(LLMs),基本上是经过强化训练的 n-gram 模型,它们在网络规模的语言语料库(实际上,可以说是我们文明的知识库)上进行了训练,展现出了一种超乎预期的语言行为,引发了我们的广泛关注。从训练和操作的角度来看,LLMs 可以被认为是一种巨大的、非真实的记忆库,相当于为我们所有人提供了一个外部的系统 1(见图 1)。然而,它们表面上的多功能性让许多研究者好奇,这些模型是否也能在通常需要系

人工和AI大语言模型成本对比 ai语音模型

这里既有AI,又有生活大道理,无数渺小的思考填满了一生。 上一专题搭建了一套GMM-HMM系统,来识别连续0123456789的英文语音。 但若不是仅针对数字,而是所有普通词汇,可能达到十几万个词,解码过程将非常复杂,识别结果组合太多,识别结果不会理想。因此只有声学模型是完全不够的,需要引入语言模型来约束识别结果。让“今天天气很好”的概率高于“今天天汽很好”的概率,得到声学模型概率高,又符合表达

C语言 将“China”译成密码

将“China”译成密码,密码规律是:用原来的字母后面的第4个字母代替原来的字母。例如,字母“A”后面的第4个字母是“E”,用“E”代替“A”。因此,“China”应译为“Glmre”。编译程序用付赋初值的方法使c1,c2,c3,c4,c5这五个变量的值分别为“C”,“h”,“i”,“n”,“a”,经过运算,使c1,c2,c3,c4,c5分别变成“G”,“l”,“m”,“r”,“e”。分别用put

C语言入门系列:探秘二级指针与多级指针的奇妙世界

文章目录 一,指针的回忆杀1,指针的概念2,指针的声明和赋值3,指针的使用3.1 直接给指针变量赋值3.2 通过*运算符读写指针指向的内存3.2.1 读3.2.2 写 二,二级指针详解1,定义2,示例说明3,二级指针与一级指针、普通变量的关系3.1,与一级指针的关系3.2,与普通变量的关系,示例说明 4,二级指针的常见用途5,二级指针扩展到多级指针 小结 C语言的学习之旅中,二级

【LinuxC语言】select轮询

文章目录 前言select函数详解selectfd_set类型一个小问题select函数使用步骤改进服务器代码select服务器示例代码 总结 前言 在Linux C语言编程中,我们经常需要处理多个I/O操作。然而,如果我们为每个I/O操作创建一个线程,那么当I/O操作数量增加时,线程管理将变得复杂且效率低下。这就是我们需要select轮询的地方。select是一种高效的I/

拓扑排序——C语言

拓扑排序(Topological Sorting)是一种用于有向无环图(DAG)的排序算法,其输出是图中所有顶点的线性排序,使得对于每条有向边 (u, v),顶点 u 在 v 之前出现。拓扑排序确定了项目网络图中的起始事件和终止事件,也就是顶点的执行顺序。         因为是有向无环图,所以拓扑排序的作用其实就是把先发生的排序在前面,后发生的排序到后面。 例如现在我们有一个

OC和 C语言中的const

const与宏对比 1.都是在其他的地方不可以改变 2.一个地方改了其他的地方都会改变。 而且宏定义的缺陷是, 是它会不断的开辟临时变量的存储空间 使用const的话 是都去使用同一的一份空间,使用同一个对象。 加const 之后变量还是全局的,只不过变为全局常量。 如果此时改变量不想被被类外面访问的话,可以加上static关键字, 3.下次想要定义一些宏的时候分

C语言入门系列:初识函数

文章目录 一,C语言函数与数学函数的区别1,回忆杀-初中数学2,C语言中的函数 二, 函数的声明1,函数头1.1,函数名称1.2,返回值类型1.3,参数列表 2,函数体2.1,函数体2.2,return语句 三,main函数四,函数的参数与传递方式1,实参和形参1.1,函数定义(含形参)1.2,函数调用(使用实参) 2,参数传递方式2.1,值传递2.2,引用传递 五,函数原型与预声明1,