Linux SockFS文件系统分析4(基于Linux6.6)---socket 系统调用介绍
一、概述
以下是一些与套接字相关的重要系统调用概述:
1. socket()
socket()
系统调用用于创建一个新的套接字,并返回一个套接字描述符(fd
),该套接字可以用于后续的读写操作。这个描述符是与进程生命周期关联的。
原型:
int socket(int domain, int type, int protocol);
参数:
domain
:指定协议族(例如:AF_INET
、AF_INET6
表示 IPv4 和 IPv6,AF_UNIX
表示 Unix 域套接字)。type
:指定套接字类型(例如:SOCK_STREAM
表示 TCP 套接字,SOCK_DGRAM
表示 UDP 套接字)。protocol
:指定使用的协议(通常为 0,由系统自动选择)。对于SOCK_STREAM
类型,通常选择IPPROTO_TCP
,对于SOCK_DGRAM
类型,通常选择IPPROTO_UDP
。
返回值:
- 返回一个套接字文件描述符(
fd
),如果出错返回-1
,并设置errno
。
2. bind()
bind()
系统调用将套接字与本地地址(IP 地址和端口号)绑定。服务器端通常使用此调用将套接字与特定的地址和端口绑定,以便监听客户端连接。
原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
:通过socket()
创建的套接字描述符。addr
:指向struct sockaddr
结构的指针,包含绑定的地址信息(例如 IP 地址和端口)。addrlen
:addr
结构的大小。
返回值:
- 成功返回 0,失败返回 -1,
errno
设置为相应的错误代码。
3. listen()
listen()
系统调用用于将一个套接字设置为监听状态,等待客户端的连接请求。这个调用主要用于服务器端,准备接受客户端的连接。
原型:
int listen(int sockfd, int backlog);
参数:
sockfd
:套接字描述符。backlog
:最大等待连接队列的长度。如果有多个客户端连接请求,backlog
参数指定了操作系统内部的等待队列长度。
返回值:
- 成功返回 0,失败返回 -1,
errno
设置为相应的错误代码。
4. accept()
accept()
系统调用用于接受一个已连接的客户端套接字,服务器端调用此函数以接收并处理客户端的连接请求。accept()
会阻塞,直到有连接请求到来。
原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd
:监听套接字描述符。addr
:指向struct sockaddr
结构的指针,用于返回客户端的地址信息。addrlen
:指向地址长度的指针,函数返回时会填充实际的地址长度。
返回值:
- 返回一个新的套接字描述符,代表已连接的客户端,成功时返回该描述符,失败时返回 -1,
errno
设置为相应的错误代码。
5. connect()
connect()
系统调用用于客户端与服务器建立连接。客户端使用此调用向指定的服务器地址发送连接请求。对于 TCP 套接字,connect()
会触发三次握手过程。
原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
:客户端套接字描述符。addr
:指向服务器地址的struct sockaddr
结构。addrlen
:地址结构的大小。
返回值:
- 成功返回 0,失败返回 -1,
errno
设置为相应的错误代码。
6. send() / sendto()
send()
和 sendto()
系统调用用于向套接字发送数据。send()
通常用于连接已建立的套接字,而 sendto()
适用于无连接的套接字(如 UDP)。
send()
用于 TCP 连接,发送数据到已连接的对端。sendto()
可以用于 TCP 和 UDP 套接字,适用于无连接的通信。
原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
sockfd
:套接字描述符。buf
:指向要发送数据的缓冲区。len
:发送数据的长度。flags
:指定发送行为的标志,通常为 0。dest_addr
和addrlen
(仅sendto()
):指定目标地址和地址长度(对于 UDP)。
返回值:
- 返回成功发送的字节数,出错时返回 -1,
errno
设置为相应的错误代码。
7. recv() / recvfrom()
recv()
和 recvfrom()
系统调用用于从套接字接收数据。recv()
通常用于连接已建立的套接字,而 recvfrom()
适用于无连接的套接字(如 UDP)。
recv()
用于从 TCP 套接字接收数据。recvfrom()
可以用于 TCP 和 UDP 套接字,适用于无连接的通信。
原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
sockfd
:套接字描述符。buf
:接收数据的缓冲区。len
:要接收的最大字节数。flags
:指定接收行为的标志。src_addr
和addrlen
(仅recvfrom()
):用于接收方地址和地址长度(对于 UDP)。
返回值:
- 返回接收到的字节数,出错时返回 -1,
errno
设置为相应的错误代码。
8. close()
close()
系统调用用于关闭套接字。关闭套接字后,套接字描述符不再有效,并释放相应的资源。
原型:
int close(int fd);
参数:
fd
:套接字描述符。
返回值:
- 成功返回 0,失败返回 -1,
errno
设置为相应的错误代码。
9. shutdown()
shutdown()
系统调用用于关闭套接字的某一方向的读写操作,通常用于 TCP 套接字,在关闭连接之前,应用程序可以选择关闭读、写或双方的操作。
原型:
int shutdown(int sockfd, int how);
参数:
sockfd
:套接字描述符。how
:关闭方式,取值可以是:SHUT_RD
:关闭读操作。SHUT_WR
:关闭写操作。SHUT_RDWR
:同时关闭读写操作。
返回值:
- 成功返回 0,失败返回 -1,
errno
设置为相应的错误代码。
针对socket而言,其相关的系统调用有socket、listen、accept、shutdown、connect、bind、recvmsg、getsockname、sendmsg、send、setsockopt、getsockopt等,此处我们以setsockopt进行分析说明,其他的系统调用也是类似的。
二、setsockopt接口分析
net/caif/caif_socket.c
static int setsockopt(struct socket *sock, int lvl, int opt, sockptr_t ov,
unsigned int ol)
{
struct sock *sk = sock->sk;
struct caifsock *cf_sk = container_of(sk, struct caifsock, sk);
int linksel;
if (cf_sk->sk.sk_socket->state != SS_UNCONNECTED)
return -ENOPROTOOPT;
switch (opt) {
case CAIFSO_LINK_SELECT:
if (ol < sizeof(int))
return -EINVAL;
if (lvl != SOL_CAIF)
goto bad_sol;
if (copy_from_sockptr(&linksel, ov, sizeof(int)))
return -EINVAL;
lock_sock(&(cf_sk->sk));
cf_sk->conn_req.link_selector = linksel;
release_sock(&cf_sk->sk);
return 0;
case CAIFSO_REQ_PARAM:
if (lvl != SOL_CAIF)
goto bad_sol;
if (cf_sk->sk.sk_protocol != CAIFPROTO_UTIL)
return -ENOPROTOOPT;
lock_sock(&(cf_sk->sk));
if (ol > sizeof(cf_sk->conn_req.param.data) ||
copy_from_sockptr(&cf_sk->conn_req.param.data, ov, ol)) {
release_sock(&cf_sk->sk);
return -EINVAL;
}
cf_sk->conn_req.param.size = ol;
release_sock(&cf_sk->sk);
return 0;
default:
return -ENOPROTOOPT;
}
return 0;
bad_sol:
return -ENOPROTOOPT;
}
该接口主要是设置socket相关的一些属性参数,针对tcp协议而言,可以设置tcp保活机制相关的参数(TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT)等。
首先我们看下setsockopt的定义,该接口的处理流程如下所示
该接口主要包括两个大的方向的内容:
1.根据fd获取struct socket类型的指针(通过调用sockfd_lookup_light实现);
2.根据本次设置的opt类型,选择对应的设置接口:
- 若为socket通用的opt类型(即opt类型为SOL_SOCKET),则调用sock_setsockopt进行处理;
- 若设置的opt类型为具体socket类型(SOCK_STREAM/SOCK_DGRAM等)相关联的,调用socket类型具体的操作处理接口(即由struct proto_ops类型定义的指针变量)。
以上即为setsockopt处理的流程以及主要功能点,针对sockfd_lookup_light而言,其通过fd获取文件描述符,再通过文件描述符获取socket,该接口的定义相对简单,如上流程图所示,不再展开细说。
而针对opt的处理接口,存在两个处理接口,针对通用的处理接口sock_setsockopt,主要是设置socket相关的通用动作,本文也不再细说。本次主要说明下socket类型相关的通用动作的设置。
net/ipv4/af_inet.c
const struct proto_ops inet_stream_ops = {
.family = PF_INET,
.owner = THIS_MODULE,
.release = inet_release,
.bind = inet_bind,
.connect = inet_stream_connect,
.socketpair = sock_no_socketpair,
.accept = inet_accept,
.getname = inet_getname,
.poll = tcp_poll,
.ioctl = inet_ioctl,
.gettstamp = sock_gettstamp,
.listen = inet_listen,
.shutdown = inet_shutdown,
.setsockopt = sock_common_setsockopt,
.getsockopt = sock_common_getsockopt,
.sendmsg = inet_sendmsg,
.recvmsg = inet_recvmsg,
#ifdef CONFIG_MMU
.mmap = tcp_mmap,
#endif
.splice_eof = inet_splice_eof,
.splice_read = tcp_splice_read,
.read_sock = tcp_read_sock,
.read_skb = tcp_read_skb,
.sendmsg_locked = tcp_sendmsg_locked,
.peek_len = tcp_peek_len,
#ifdef CONFIG_COMPAT
.compat_ioctl = inet_compat_ioctl,
#endif
.set_rcvlowat = tcp_set_rcvlowat,
};
EXPORT_SYMBOL(inet_stream_ops);
此处我们以tcp协议为例,当我们创建一个tcp协议链接时,socket类型选择的为SOCK_STREAM(该类
型的struct proto_ops类型变量的的定义如上),因此sock->ops->setsockopt调用的接口即为sock_common_setsockopt,该接口的定义如下,即调用具体协议相关的setsockopt,接下来我们确认下tcp协议的setsockopt是哪一个函数。
net/core/sock.c
/*
* Set socket options on an inet socket.
*/
int sock_common_setsockopt(struct socket *sock, int level, int optname,
sockptr_t optval, unsigned int optlen)
{
struct sock *sk = sock->sk;
/* IPV6_ADDRFORM can change sk->sk_prot under us. */
return READ_ONCE(sk->sk_prot)->setsockopt(sk, level, optname, optval, optlen);
}
EXPORT_SYMBOL(sock_common_setsockopt);
针对tcp协议而言,我们知道其sk_proto即为tcp_prot(关于sk->sk_prot的赋值过程,请参考文章《LINUX 套接字文件系统(sockfs)分析之三 socket fd创建的过程》,关于struct proto、struct proto_ops结构体的关联,请参考文章《LINUX 套接字文件系统(sockfs)分析之二 相关结构体分析》),该变量的定义如下,因此其setsockopt接口为tcp_setsockopt,tcp_setsockopt的定义如下所示,主要调用do_tcp_setsockopt接口实现tcp相关动作的操作,do_tcp_setsockopt实现了针对TCP_MAXSEG、TCP_NODELAY、TCP_THIN_LINEAR_TIMEOUTS、TCP_THIN_DUPACK、TCP_REPAIR、TCP_KEEPIDLE、TCP_KEEPINTVL、TCP_KEEPCNT等opt类型的支持,因do_tcp_setsockopt主要是根据opt的类型进行相应的设置操作。
net/ipv4/tcp.c
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.bpf_bypass_getsockopt = tcp_bpf_bypass_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
.splice_eof = tcp_splice_eof,
.backlog_rcv = tcp_v4_do_rcv,
.release_cb = tcp_release_cb,
.hash = inet_hash,
.unhash = inet_unhash,
.get_port = inet_csk_get_port,
.put_port = inet_put_port,
#ifdef CONFIG_BPF_SYSCALL
.psock_update_sk_prot = tcp_bpf_update_proto,
#endif
.enter_memory_pressure = tcp_enter_memory_pressure,
.leave_memory_pressure = tcp_leave_memory_pressure,
.stream_memory_free = tcp_stream_memory_free,
.sockets_allocated = &tcp_sockets_allocated,
.orphan_count = &tcp_orphan_count,
.memory_allocated = &tcp_memory_allocated,
.per_cpu_fw_alloc = &tcp_memory_per_cpu_fw_alloc,
.memory_pressure = &tcp_memory_pressure,
.sysctl_mem = sysctl_tcp_mem,
.sysctl_wmem_offset = offsetof(struct net, ipv4.sysctl_tcp_wmem),
.sysctl_rmem_offset = offsetof(struct net, ipv4.sysctl_tcp_rmem),
.max_header = MAX_TCP_HEADER,
.obj_size = sizeof(struct tcp_sock),
.slab_flags = SLAB_TYPESAFE_BY_RCU,
.twsk_prot = &tcp_timewait_sock_ops,
.rsk_prot = &tcp_request_sock_ops,
.h.hashinfo = NULL,
.no_autobind = true,
.diag_destroy = tcp_abort,
};
EXPORT_SYMBOL(tcp_prot);
int tcp_setsockopt(struct sock *sk, int level, int optname, sockptr_t optval,
unsigned int optlen)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
if (level != SOL_TCP)
/* Paired with WRITE_ONCE() in do_ipv6_setsockopt() and tcp_v6_connect() */
return READ_ONCE(icsk->icsk_af_ops)->setsockopt(sk, level, optname,
optval, optlen);
return do_tcp_setsockopt(sk, level, optname, optval, optlen);
}
EXPORT_SYMBOL(tcp_setsockopt);
三、举例应用
下面是一个简单的 TCP 服务器 和 TCP 客户端 的示例,展示了如何使用 Linux 套接字系统调用来处理网络通信。
1. TCP 服务器端实现(Server)
服务器端的任务是:
- 创建一个套接字。
- 将套接字绑定到一个特定的地址(IP 和端口)。
- 监听连接请求。
- 接受客户端的连接。
- 接收客户端的数据并进行处理。
服务器端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
#define MAX_CONN 5
int main() {
int server_fd, new_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[1024] = {0};
// 1. 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket failed");
exit(EXIT_FAILURE);
}
// 2. 配置服务器地址结构
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 任意可用的网络接口
server_addr.sin_port = htons(PORT); // 端口号
// 3. 将套接字与地址绑定
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 4. 监听连接请求
if (listen(server_fd, MAX_CONN) < 0) {
perror("Listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d\n", PORT);
// 5. 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&client_addr, &client_len)) < 0) {
perror("Accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 6. 打印客户端信息
printf("Connection established with %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 7. 接收数据
int bytes_received = read(new_socket, buffer, sizeof(buffer));
if (bytes_received > 0) {
printf("Received message: %s\n", buffer);
}
// 8. 关闭套接字
close(new_socket);
close(server_fd);
return 0;
}
2. TCP 客户端实现(Client)
客户端的任务是:
- 创建一个套接字。
- 连接到服务器的地址(IP 和端口)。
- 发送数据给服务器。
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8080
int main() {
int sock = 0;
struct sockaddr_in server_addr;
char *message = "Hello from client";
// 1. 创建套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket failed");
exit(EXIT_FAILURE);
}
// 2. 配置服务器地址结构
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
// 将IPv4地址从文本转换为二进制
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("Invalid address");
close(sock);
exit(EXIT_FAILURE);
}
// 3. 连接到服务器
if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Connection failed");
close(sock);
exit(EXIT_FAILURE);
}
// 4. 发送数据
send(sock, message, strlen(message), 0);
printf("Message sent: %s\n", message);
// 5. 关闭套接字
close(sock);
return 0;
}
3. 执行过程与系统调用处理
假设我们先运行服务器端程序,再运行客户端程序。服务器端与客户端之间的通信是通过套接字完成的,以下是每个关键步骤的系统调用处理过程:
服务器端(Server)执行过程:
-
socket()
:- 创建一个套接字,返回一个套接字描述符。
- 系统为该套接字分配资源。
-
bind()
:- 将套接字与一个 IP 地址(如
INADDR_ANY
表示所有可用接口)和端口号(如 8080)绑定。 - 内核创建一个
bind
关联,并把套接字与该地址绑定在一起。
- 将套接字与一个 IP 地址(如
-
listen()
:- 将套接字设置为监听模式,等待客户端连接。
- 内核将开始监听来自客户端的连接请求,并维护一个等待队列。
-
accept()
:- 等待客户端连接,一旦有连接请求到达,接受客户端的连接并创建一个新的套接字。
- 内核返回一个新的套接字描述符,表示与客户端的连接。
accept()
会填充客户端的地址信息,之后服务器可以与客户端进行数据交换。
-
read()
:- 读取客户端发送的消息,系统调用会从客户端发送的数据缓冲区中取出数据。
- 服务器端处理接收到的数据(例如打印消息)。
-
close()
:- 关闭与客户端的连接以及服务器的监听套接字,释放资源。
客户端(Client)执行过程:
-
socket()
:- 创建一个客户端套接字,返回一个套接字描述符。
- 系统为客户端套接字分配资源。
-
connect()
:- 客户端使用
connect()
连接到服务器。 - 内核通过三次握手建立与服务器的 TCP 连接。
- 客户端使用
-
send()
:- 客户端通过
send()
发送数据到服务器。 - 数据被写入发送缓冲区,内核将数据通过网络传输给服务器。
- 客户端通过
-
close()
:- 关闭客户端套接字,释放资源。
4. 核心系统调用与内核处理过程
socket()
: 内核为套接字分配内存和资源,并返回套接字文件描述符。内核会根据domain
和type
创建适合的协议控制块(如 TCP、UDP 等)。bind()
: 内核检查指定的地址是否可以用于绑定。如果可以,它会将套接字与该地址绑定,并在内核的网络栈中建立相关的映射。listen()
: 内核为监听套接字分配队列,用于保存待处理的连接请求。accept()
: 内核从等待队列中取出客户端连接,创建一个新的套接字描述符,并将其返回给应用程序。send()
/recv()
: 内核将数据从发送缓冲区发送到网络,或者将接收到的数据从网络传递到接收缓冲区。close()
: 内核释放与套接字相关的所有资源,关闭套接字并返回文件描述符。