Linux SockFS文件系统分析4

Linux SockFS文件系统分析4(基于Linux6.6)---socket 系统调用介绍

一、概述

以下是一些与套接字相关的重要系统调用概述:

1. socket()

socket() 系统调用用于创建一个新的套接字,并返回一个套接字描述符(fd),该套接字可以用于后续的读写操作。这个描述符是与进程生命周期关联的。

原型

int socket(int domain, int type, int protocol);

参数

  • domain:指定协议族(例如:AF_INETAF_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 地址和端口)。
  • addrlenaddr 结构的大小。

返回值

  • 成功返回 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_addraddrlen(仅 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_addraddrlen(仅 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类型,选择对应的设置接口:

  1. 若为socket通用的opt类型(即opt类型为SOL_SOCKET),则调用sock_setsockopt进行处理;
  2. 若设置的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)

服务器端的任务是:

  1. 创建一个套接字。
  2. 将套接字绑定到一个特定的地址(IP 和端口)。
  3. 监听连接请求。
  4. 接受客户端的连接。
  5. 接收客户端的数据并进行处理。

服务器端代码:

#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)

客户端的任务是:

  1. 创建一个套接字。
  2. 连接到服务器的地址(IP 和端口)。
  3. 发送数据给服务器。

客户端代码:

#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)执行过程:

  1. socket():

    • 创建一个套接字,返回一个套接字描述符。
    • 系统为该套接字分配资源。
  2. bind():

    • 将套接字与一个 IP 地址(如 INADDR_ANY 表示所有可用接口)和端口号(如 8080)绑定。
    • 内核创建一个 bind 关联,并把套接字与该地址绑定在一起。
  3. listen():

    • 将套接字设置为监听模式,等待客户端连接。
    • 内核将开始监听来自客户端的连接请求,并维护一个等待队列。
  4. accept():

    • 等待客户端连接,一旦有连接请求到达,接受客户端的连接并创建一个新的套接字。
    • 内核返回一个新的套接字描述符,表示与客户端的连接。
    • accept() 会填充客户端的地址信息,之后服务器可以与客户端进行数据交换。
  5. read():

    • 读取客户端发送的消息,系统调用会从客户端发送的数据缓冲区中取出数据。
    • 服务器端处理接收到的数据(例如打印消息)。
  6. close():

    • 关闭与客户端的连接以及服务器的监听套接字,释放资源。

客户端(Client)执行过程:

  1. socket():

    • 创建一个客户端套接字,返回一个套接字描述符。
    • 系统为客户端套接字分配资源。
  2. connect():

    • 客户端使用 connect() 连接到服务器。
    • 内核通过三次握手建立与服务器的 TCP 连接。
  3. send():

    • 客户端通过 send() 发送数据到服务器。
    • 数据被写入发送缓冲区,内核将数据通过网络传输给服务器。
  4. close():

    • 关闭客户端套接字,释放资源。

4. 核心系统调用与内核处理过程

  • socket(): 内核为套接字分配内存和资源,并返回套接字文件描述符。内核会根据 domaintype 创建适合的协议控制块(如 TCP、UDP 等)。
  • bind(): 内核检查指定的地址是否可以用于绑定。如果可以,它会将套接字与该地址绑定,并在内核的网络栈中建立相关的映射。
  • listen(): 内核为监听套接字分配队列,用于保存待处理的连接请求。
  • accept(): 内核从等待队列中取出客户端连接,创建一个新的套接字描述符,并将其返回给应用程序。
  • send()/recv(): 内核将数据从发送缓冲区发送到网络,或者将接收到的数据从网络传递到接收缓冲区。
  • close(): 内核释放与套接字相关的所有资源,关闭套接字并返回文件描述符。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值