一、Linux 4.19内核accept 系统调用代码中文注释
/** 在使用accept时,我们尝试创建一个新的socket,与客户端建立连接,* 唤醒客户端,然后返回新的连接文件描述符(fd)。我们在内核空间收集* 连接方的地址,并在最后将其移到用户空间。这样做不干净是因为,我们* 打开socket后可能返回一个错误。** 1003.1g标准增加了通过recvmsg()查询连接挂起状态的能力。我们* 需要在重构accept时,也以一种干净的方式增加这个支持。*/int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,int __user *upeer_addrlen, int flags)
{struct socket *sock, *newsock;struct file *newfile;int err, len, newfd, fput_needed;struct sockaddr_storage address;// 校验传入的flags标志if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))return -EINVAL;// 将SOCK_NONBLOCK转换为对应的O_NONBLOCK标志if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;// 通过文件描述符找到对应的socket,fd为文件描述符sock = sockfd_lookup_light(fd, &err, &fput_needed);if (!sock)goto out;// ENFILE表示文件表溢出err = -ENFILE;newsock = sock_alloc(); // 分配一个新的socketif (!newsock)goto out_put;// 设置新socket的类型和操作函数newsock->type = sock->type;newsock->ops = sock->ops;/** 我们不需要调用try_module_get,因为监听socket(sock)* 已经有了协议模块(sock->ops->owner)。*/__module_get(newsock->ops->owner);// 获得未使用的文件描述符newfd = get_unused_fd_flags(flags);if (unlikely(newfd < 0)) {err = newfd;sock_release(newsock); // 释放socket资源goto out_put;}// 分配新socket文件表项newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);if (IS_ERR(newfile)) {err = PTR_ERR(newfile);put_unused_fd(newfd);goto out_put;}// 安全检查err = security_socket_accept(sock, newsock);if (err)goto out_fd;// 调用实际的accept操作err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);if (err < 0)goto out_fd;// 如果用户提供了地址存储,则获取并复制给用户if (upeer_sockaddr) {len = newsock->ops->getname(newsock,(struct sockaddr *)&address, 2);if (len < 0) {err = -ECONNABORTED;goto out_fd;}err = move_addr_to_user(&address,len, upeer_sockaddr, upeer_addrlen);if (err < 0)goto out_fd;}/* 与某些操作系统不同,文件标志不是通过accept()继承的。 */// 将新的文件描述符安装到文件表中fd_install(newfd, newfile);err = newfd;out_put:fput_light(sock->file, fput_needed); // 释放文件表项
out:return err; // 返回错误码或新的文件描述符
out_fd:fput(newfile); // 释放文件表项put_unused_fd(newfd); // 释放未使用的文件描述符goto out_put;
}// 处理accept4系统调用,将用户态参数转到内核态处理
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,int __user *, upeer_addrlen, int, flags)
{return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, flags);
}// 处理accept系统调用,flags默认为0
SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,int __user *, upeer_addrlen)
{return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}
这段代码定义了两个系统调用函数:
- SYSCALL_DEFINE4(accept4, ...): 这是一个宏,它定义了`accept4`系统调用的接口。`accept4`允许用户程序在创建一个新的socket连接时指定额外的选项,比如`SOCK_CLOEXEC`和`SOCK_NONBLOCK`。它接收四个参数:文件描述符`fd`、用户空间的指向`sockaddr`结构的指针`upeer_sockaddr`、指向地址长度变量的指针`upeer_addrlen`、以及标志位`flags`。内核会将这个调用委托给`__sys_accept4`处理。
- SYSCALL_DEFINE3(accept, ...): 这个宏定义了标准的`accept`系统调用,它不接收`flags`参数(默认为0),只在`accept4`的基础上省略了最后一个参数。其余的参数和`accept4`相同。内核也会将这个调用委托给`__sys_accept4`处理,只不过`flags`参数提供了默认值0。
这两个宏最终都会导致`__sys_accept4`函数被调用来执行处理连接请求的实际工作。
- upeer_sockaddr和`upeer_addrlen`是指向用户空间的指针,表明`accept`和`accept4`在连接成功之后,可以返回新连接端点的地址信息给用户程序。
这部分的代码是Linux内核网络栈中处理接受新连接请求的关键组成部分。这些函数直接与用户空间的应用程序交互,允许它们在监听的网络端口上接受新的连接。
二、代码解释
这段代码是Linux内核4.19中处理`accept`系统调用的实现,其作用是允许服务器接受一个来自客户端的连接请求。以下是解读:
这个函数尝试创建一个新的socket,设置与客户端的链接,唤醒客户端,然后返回新创建的已连接的文件描述符(fd)。它会在内核空间收集连接方的地址信息,并将信息复制到用户空间。
这里定义了两个系统调用的接口函数:`SYSCALL_DEFINE3(accept, ...)`和`SYSCALL_DEFINE4(accept4, ...)。`accept`是基本的接受连接的系统调用,而`accept4`是一个增强版,允许用户通过额外的`flags`参数设置一些选项。
__sys_accept4函数做了以下工作:
1. 首先检查传入的`flags`参数,确保用户没有设置不合法的标志,如果是就返回`-EINVAL`(表示无效的参数)。
2. 调用`sockfd_lookup_light`函数,尝试获取与给定文件描述符`fd`关联的socket对象。如果成功,继续后续操作;否则,返回错误。
3. 分配一个新的socket结构给新的连接,并拷贝被监听socket的一些属性。
4. 获取一个未使用的文件描述符`newfd`,分配并初始化一个新的文件对象`newfile`。如果这些操作发生错误,清理并返回错误码。
5. 调用`security_socket_accept`和`sock->ops->accept`进行安全检查和实际的接受操作。这些步骤涉及底层协议的操作,以处理连接建立等细节。
6. 如果用户提供了地址存储空间(`upeer_sockaddr`),将新socket的地址信息从内核空间复制到用户空间。
7. 使用`fd_install`将新的文件对象`newfile`绑定到新的文件描述符`newfd`。
8. 进行清理工作,如果在任何一步出错,释放之前分配的资源,并返回错误码;若无误,返回新的连接文件描述符`newfd`。
该函数考虑了错误处理路径,在任何可能的错误点,都会释放占用的资源以避免内存泄露。执行成功时将返回一个代表新连接的文件描述符。
这段代码是在Linux内核中处理`accept`与`accept4`系统调用的实现,这些系统调用是由服务器程序使用的,它们允许服务器接受来自客户端的连接请求。继续分步解释这个函数的执行流程。
1. 检查传入的`flags`参数:
- 若`flags`包含了不是`SOCK_CLOEXEC`或`SOCK_NONBLOCK`的其他标志位,则返回错误`-EINVAL`。
- 如果`flags`指定了非阻塞标志`SOCK_NONBLOCK`,并且这个标志和系统定义的`O_NONBLOCK`不相同,则将`flags`中的`SOCK_NONBLOCK`标志替换成`O_NONBLOCK`。
2. 调用`sockfd_lookup_light`函数来查找与文件描述符`fd`对应的socket结构体。如果没有找到,设置错误号`err`,并立即跳到`out`标签处处理退出。
3. 尝试分配一个新的socket(`newsock`)给即将建立的连接。如果分配失败,设置错误号`err`为`-ENFILE`(文件表溢出),然后跳到`out_put`标签处释放已引用的socket并处理退出。
4. 尝试获取一个未使用的文件描述符`newfd`,并为这个文件描述符创建一个新的文件对象`newfile`对应于新的socket。如果获取文件描述符失败或者创建文件对象失败,设置相应的错误号,释放资源,然后退出。
5. 调用`security_socket_accept`函数进行安全检查,并调用原始socket的`accept`方法以完成连接的建立。如果这些步骤中任何一个失败了,跳到`out_fd`来处理错误。
6. 如果用户程序提供了一个地址用于存储客户端的信息(`upeer_sockaddr`),那么获取新socket的地址信息,并通过`move_addr_to_user`函数将地址信息复制到用户空间。如果这个过程中出现错误,设置错误号并跳到`out_fd`处理错误。
7. 通过`fd_install`函数将文件对象`newfile`与文件描述符`newfd`关联起来,完成文件描述符的安装。
8. 得到成功的结果`err`,这是新的文件描述符,这意味着可以通过这个描述符与客户端通信。
如果在这个过程的任何一步中出现了错误,代码会跳到错误处理的部分,执行清理工作,确保不会有资源泄露。正确执行的流程结束时,返回新打开的文件描述符`newfd`用于后续通信。
最后,定义了`SYSCALL_DEFINE4`和`SYSCALL_DEFINE3`宏,这两个宏是用来声明接受4个参数和3个参数的系统调用接口。它们确保了`accept`和`accept4`系统调用可以通过系统调用接口被用户空间的程序访问。这些宏最终将调用`__sys_accept4`来完成实际的工作。如果使用`accept`而非`accept4`,那么`flags`参数默认为0,表示标准的`accept`行为。