目录
前言
学习Linux网络编程,接触的第一个函数是socket(),在过往使用中,并没有真正搞懂操作系统做了什么。本文旨在记录学习Linux网络子系统源码的过程与思考,不死扣代码细节,仅聚焦于核心逻辑和数据流,希望能和同样对Linux网络感兴趣的朋友一起探讨学习。
一、一行代码,看懂入口
如无特殊说明,代码以TCP为例
int fd = socket(int domain, int type, int protocol);
socket 的类型由三个参数决定:
- domain:协议族,决定套接字的寻址方式
- AF_INET,使用IPv4协议
- AF_INET6:使用IPv6协议
- type:套接字类型,决定数据传输方式
- SOCK_STREAM,提供有序、可靠、双向、基于连接的字节流,如TCP
- SOCK_DGRAM,支持数据报(无连接、不可靠的、具有固定最大长度的消息),如UDP
- protocol:具体传输协议,通常填0让系统选择
- SOCK_STREAM:默认使用TCP
- SOCK_DGRAM:默认使用UDP
返回值:
- 成功:返回一个非负整数的文件描述符
- 失败:-1
二、重点:调用 socket() 时,内核默默做了什么事情
1.调用框图
2.关键结构体
2.1 操作集
// IPv4协议族
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
// 该数组定义了 IPv4 协议族下 TCP、UDP、ICMP 和 RAW 四种套接字类型的协议实现(prot)与操作接口(ops)
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
// 定义了协议本身的具体实现,负责具体协议算法的实现
.prot = &tcp_prot,
// 定义了套接字层的操作接口,负责将用户空间的请求转化为内核协议栈的调用
.ops = &inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
},
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP,
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}
};
// IPv4 地址族专有操作
const struct inet_connection_sock_af_ops ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = inet_sk_rebuild_header,
.sk_rx_dst_set = inet_sk_rx_dst_set,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = inet_csk_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
.bind_conflict = inet_csk_bind_conflict,
.mtu_reduced = tcp_v4_mtu_reduced,
};
2.2 socket 相关结构体
struct socket
struct sock
struct inet_sock
struct inet_connection_sock
struct tcp_sock
在 Linux 内核网络协议栈中,struct socket、struct sock、struct inet_sock、struct inet_connection_sock和 struct tcp_sock是层层嵌套、逐步特化的数据结构,共同构成了从通用套接字接口到具体 TCP 连接控制块的完整抽象链。整体继承关系如下:
3.调用过程
socket的相关函数都在net\socket.c中,socket()方法主要调用了两个函数,如下:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
// 创建 socket 对象
retval = sock_create(family, type, protocol, &sock);
// 将创建好的 struct socket 对象与一个文件描述符绑定,并返回该文件描述符给用户空间
retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
3.1 sock_create
该函数调用 __sock_create() ,其关键代码如下:
int __sock_create(struct net *net, int family, int type, int protocol, struct socket **res, int kern)
{
// 分配一个 struct socket 对象
struct socket *sock;
sock = sock_alloc();
// 协议族 net_proto_family 通过 net/ipv4/af_inet.c 中的 sock_register(&inet_family_ops) 注册进来
pf = rcu_dereference(net_families[family]);
// AF_INET 对应的是 int inet_create(...)
err = pf->create(net, sock, protocol, kern);
}
inet_create主要做的工作包括:
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
struct sock *sk;
struct inet_sock *inet;
// 设置监听 socket 状态为 未连接状态
sock->state = SS_UNCONNECTED;
list_for_each_entry_rcu(answer, &inetsw[sock->type], list)
{
// inet_stream_ops 操作集赋值到 socket 上
sock->ops = answer->ops;
// 获取 tcp_prot
answer_prot = answer->prot;
// 分配一个 struct sock 对象,并将其与指定的协议族(IPv4)、协议实现(tcp_prot)关联起来
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
// 初始化 sock 对象,并将 socket 和 sock 关联起来
sock_init_data(sock, sk);
// 初始化TCP套接字,实际调用 tcp_prot 中的 tcp_v4_init_sock 方法
err = sk->sk_prot->init(sk);
}
}
sock_init_data主要工作:
void sock_init_data(struct socket *sock, struct sock *sk)
{
// 接收队列 已经到达内核、等待用户 recvmsg()取走的 skb,初始化为空链表
skb_queue_head_init(&sk->sk_receive_queue);
// 发送队列 已发送但尚未得到 ACK或待发送的 skb,初始化为空链表
skb_queue_head_init(&sk->sk_write_queue);
// 设置监听 sock 状态为 关闭状态
sk->sk_state = TCP_CLOSE;
// 建立双向指针:sock->sk = sk,sk->sk_socket = sock
sk_set_socket(sk, sock);
sock->sk = sk;
// 设置默认回调函数
sk->sk_state_change = sock_def_wakeup;
sk->sk_data_ready = sock_def_readable;
sk->sk_write_space = sock_def_write_space;
}
tcp_v4_init_sock的主要工作:
static int tcp_v4_init_sock(struct sock *sk)
{
// 面向连接的 socket 扩展结构体
struct inet_connection_sock *icsk = inet_csk(sk);
// 在 sock_init_data的基础上进一步设置 TCP 特有的字段,使 socket 具备 TCP 协议运行所需的状态和参数
tcp_init_sock(sk);
// 绑定 IPv4 特定的网络层操作
icsk->icsk_af_ops = &ipv4_specific;
}
tcp_init_sock的主要工作:
void tcp_init_sock(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
// 初始化乱序队列
tp->out_of_order_queue = RB_ROOT;
// 初始化TCP定时器(重传定时器、延迟 ACK 定时器、keepalive 定时器)
tcp_init_xmit_timers(sk);
// 初始化 prequeue
tcp_prequeue_init(tp);
// 设置 TCP 缓冲区大小
sk->sk_sndbuf = sysctl_tcp_wmem[1];
sk->sk_rcvbuf = sysctl_tcp_rmem[1];
}
至此,一个 SOCK_STREAM 内核对象已经创建好了。
3.2 sock_map_fd
该函数关键代码如下:
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
// 在当前进程的文件描述符表中找到一个空闲的 slot,并返回其索引
int fd = get_unused_fd_flags(flags);
// sock_alloc_file分配一个新的 struct file实例,并建立与 socket 的双向关联:
// 1. 将 sock->file指向新文件
// 2. 将 file->private_data指向 sock
newfile = sock_alloc_file(sock, flags, NULL);
}
三、总结
对socket()方法主要做了以下几件事情:
- 分配内核 socket 结构:面向用户
- 设置监听 socket 状态为 SS_UNCONNECTED
- 创建并初始化协议对应的 sock:面向网络协议栈
- 设置监听 sock 状态为 TCP_CLOSE
- 初始化接收队列、发送队列、乱序队列
- 初始化缓冲区大小
- 绑定文件描述符
- 返回文件描述符
四、后续预告
本篇是网络系统调用开篇,下一篇我们继续拆解:
bind() 到底干了什么?为什么客户端通常不需要 bind?内核端口绑定原理?
五、写在最后
以往学习过程中经常只输入不输出,这次坚持记录每一个关键知识点、每一次踩坑与复盘,不追求速成,坚持稳步积累,慢慢搭建属于自己的网络、运维、性能调优技术体系,能将实际遇到的问题转换成机器语言。
3083



被折叠的 条评论
为什么被折叠?



