一、准备环境
环境:Ubuntu18.04
编辑器:VSCode
内核版本:linux-4.19.194
1. 下载编译内核源码
wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.19.194.tar.xz
tar xf linux-4.19.194.tar.xz
cd linux-4.19.194
# 想要编译什么架构就指定什么值,所有合法的值在源码的arch文件夹下必须存在同名的文件夹
export ARCH=x86 #x86架构
# make命令的参数可以通过 make help 获取
make x86_64_defconfig # 生成64位平台的默认配置
make # 编译内核
如果想要下载与当前正在运行的 Ubuntu 内核对应的源代码,可以使用命令
apt-get source linux-image-unsigned-$(uname -r)
来获取。
2. 分析编译时的宏定义
在第一步 下载编译内核源码 成功后,源代码目录下会生成一个名为 .config
的文件,该文件指明了编译内核时所用到的设置。可以使用下面的命令来生成编译内核代码时使用的宏定义列表:
grep "^CONFIG_.*" .config | sed "s/=y/=1/g" | sed "s/=m/_MODULE=1/g" | cat - <(echo __KERNEL__)
如果想要生成与当前正在运行的 Ubuntu 内核对应的配置,使用下面的命令:
cat /boot/config-$(uname -r) | grep "^CONFIG_.*" | sed "s/=y/=1/g" | sed "s/=m/_MODULE=1/g" | cat - <(echo __KERNEL__)
最后添加的宏
__KERNEL__
来自源代码根目录下Makefile
文件的443行。
将上面命令生成的结果配置到vscode中,可以更加方面有效的查看内核代码。
二、确定套接字调用对应的内核实现函数
下面以创建套接字的函数
socket
为例。
1. 确定socket调用的syscall号
// file: main.c
// compile: gcc -ggdb main.c
#include <sys/types.h>
#include <sys/socket.h>
int main(int argc, char** argv)
{
int s = socket(AF_INET, SOCK_DGRAM, 0);
return s;
}
GDB反汇编调试socket调用
0x7ffff7b04d50 <socket> mov $0x29,%eax
0x7ffff7b04d55 <socket+5> syscall
可以发现socket调用的syscall号为0x29(即41)。
命令
man syscall
可以获取更多关于 syscall 调用的信息。
2. 确定socket对应的内核实现
系统调用表定义在 arch/x86/entry/syscall_64.c
文件中:
// file: arch/x86/entry/syscall_64.c
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
* when the & below is removed.
*/
[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};
代码中 #include <asm/syscalls_64.h>
对应的文件是 arch/x86/include/generated/asm/syscalls_64.h
:
// file: arch/x86/include/generated/asm/syscalls_64.h
#ifdef CONFIG_X86
__SYSCALL_64(41, __x64_sys_socket, )
#else /* CONFIG_UML */
__SYSCALL_64(41, sys_socket, )
#endif
arch/x86/include/generated/asm/syscalls_64.h
文件由arch/x86/entry/syscalls/syscall_64.tbl
文件生成而来。
根据 分析编译时的宏定义 的结果,可以确定宏 CONFIG_X86
是定义了的,所以调用号41对应的实现为 __x64_sys_socket
。
在源码中却找不到 __x64_sys_socket
的定义,但能在文件 net/socket.c
中找到 __sys_socket
函数的定义:
// file: net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
分析宏 SYSCALL_DEFINE3
的定义:
// file: include/linux/syscalls.h
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
接着分析宏 __SYSCALL_DEFINEx
的定义:
// file: arch/x86/include/asm/syscall_wrapper.h
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long __x64_sys##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__x64_sys##name, ERRNO); \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
asmlinkage long __x64_sys##name(const struct pt_regs *regs) \
{ \
return __se_sys##name(SC_X86_64_REGS_TO_ARGS(x,__VA_ARGS__));\
} \
__IA32_SYS_STUBx(x, name, __VA_ARGS__) \
static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
可以确定,宏 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
展开后,定义了 __x64_sys_socket
。
即 __x64_sys_socket
函数的本体就是 net/socket.c
文件中的 __sys_socket
函数。
三、分析TCP协议状态机
1. socket
调用流程如下:
__sys_socket // net/socket.c:1355
|- sock_create // net/socket.c:1346
|- __sock_create // net/socket.c:1316
socket
函数最后会调用函数 __sock_create
,其主要代码如下:
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
struct socket *sock;
const struct net_proto_family *pf;
sock = sock_alloc();
pf = rcu_dereference(net_families[family]);
pf->create(net, sock, protocol, kern);
}
sock_alloc()
函数分配套接字的一些资源后,再从 net_families
取出网络协议簇对应的处理函数,最后创建对应网络协议簇的套接字。
net_families
是一个数组,里面存放了不同的协议簇对应的处理函数。通过函数 sock_register
可以注册到此数组中。
// file: net/ipv4/af_inet.c
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,
};
static int __init inet_init(void)
{
(void)sock_register(&inet_family_ops);
}
对于 TCP/IPv4 来说,上面代码中的 err = pf->create(net, sock, protocol, kern)
调用的其实是 inet_create(net, sock, protocol, kern)
。
inet_create // net/ipv4/af_inet.c:1075
|- sock_init_data // net/core/sock.c:347
|- sk->sk_state = TCP_CLOSE // net/core/sock.c:2790
综上,调用 socket()
函数后,套接字的默认状态为 TCP_CLOSE
。
2. listen
调用流程如下:
__sys_listen // net/socket.c:1525
|- sock->ops->listen() // net/socket.c:1516
对于 TCP/IPv4 来说,上面代码中的 sock->ops->listen()
调用的其实是 net/ipv4/af_inet.c
文件中的 inet_listen
函数。
inet_listen // net/ipv4/af_inet.c:991
|- inet_csk_listen_start // net/ipv4/af_inet.c:229
|- inet_sk_state_store(sk, TCP_LISTEN) // net/ipv4/inet_connection_sock.c:923
综上,调用 listen()
函数后,套接字的状态由 TCP_CLOSE
变为 TCP_LISTEN
。
3. connect
调用流程如下:
__sys_connect // net/socket.c:1674
|- sock->ops->connect() // net/socket.c:1663
同理,对于 TCP/IPv4 来说,上面代码中的 sock->ops->connect()
调用的其实是 net/ipv4/af_inet.c
文件中的 inet_stream_connect
函数。
inet_stream_connect // net/ipv4/af_inet.c:985
|- __inet_stream_connect // net/ipv4/af_inet.c:719
|- sk->sk_prot->connect() // net/ipv4/af_inet.c:655
上面代码中的 sk->sk_prot->connect()
调用的其实是 net/ipv4/tcp_ipv4.c
文件中的 tcp_v4_connect
函数。
1. 发送SYN
tcp_v4_connect // net/ipv4/tcp_ipv4.c:2460
|- tcp_set_state(sk, TCP_SYN_SENT) // net/ipv4/tcp_ipv4.c:280
|- tcp_connect // net/ipv4/tcp_ipv4.c:318
|- tcp_transmit_skb // net/ipv4/tcp_output.c:3529
|- __tcp_transmit_skb // net/ipv4/tcp_output.c:1164
|- icsk->icsk_af_ops->queue_xmit() // net/ipv4/tcp_output.c:1148
tcp_v4_connect
函数先将套接字的状态设置为 TCP_SYN_SENT
,然后再调用 tcp_connect
发送实际的SYN包。
在函数 __tcp_transmit_skb
中构建网络包的TCP头。构建好后,调用 icsk->icsk_af_ops->queue_xmit()
发送TCP包,而函数 icsk->icsk_af_ops->queue_xmit()
实际就是 include/net/ip.h
文件中的 ip_queue_xmit
函数。
ip_queue_xmit // net/ipv4/tcp_ipv4.c:1931
|- __ip_queue_xmit // include/net/ip.h:197
|- skb_dst_set_noref(skb, &rt->dst) // net/ipv4/ip_output.c:473
|- ip_local_out // net/ipv4/ip_output.c:506
首先在函数 __ip_queue_xmit
中调用 skb_dst_set_noref(skb, &rt->dst)
设置发送的目标,然后构建 IP 头并调用函数 ip_local_out
将包含有 SYN 标志的 TCP 包发送出去。
2. 接收到SYN和ACK,并回复ACK
当 IP 层接收到 TCPv4 的包时,会调用函数 tcp_v4_rcv
来处理。
tcp_v4_rcv // net/ipv4/af_inet.c:1684
|- tcp_v4_do_rcv // net/ipv4/tcp_ipv4.c:1832
|- tcp_rcv_state_process // net/ipv4/tcp_ipv4.c:1569
|- tcp_rcv_synsent_state_process // net/ipv4/tcp_input.c:6065
|- tcp_finish_connect // net/ipv4/tcp_input.c:5898
|- tcp_set_state(sk, TCP_ESTABLISHED) // net/ipv4/tcp_input.c:5695
|- tcp_send_ack // net/ipv4/tcp_input.c:5928
在 tcp_rcv_synsent_state_process
中,先将套接字的状态设置为 TCP_ESTABLISHED
,然后再调用 tcp_send_ack
发送 ACK 完成 TCP 的三次握手。
综上,调用 connect()
函数后,套接字的状态由 TCP_CLOSE
变为 TCP_SYN_SENT
,在成功收到 ACK 后,状态再变为 TCP_ESTABLISHED
。
4. accept
调用流程如下:
__sys_accept4 // net/socket.c:1630
|- sock->ops->accept(sock, newsock) // net/socket.c:1589
对于 TCP/IPv4 来说, sock->ops->accept()
实际调用的是 net/ipv4/af_inet.c
文件中的 inet_accept
。
inet_accept // net/ipv4/af_inet.c:987
|- sk1->sk_prot->accept() // net/ipv4/af_inet.c:734
对于 TCP/IPv4 来说, sk1->sk_prot->accept()
实际调用的是 net/ipv4/inet_connection_sock.c
文件中的 inet_csk_accept
。
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo)
goto out_err;
inet_csk_wait_for_connect(sk, timeo);
}
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
return newsk;
}
inet_csk_accept
首先检查监听套接字上的连接队列是否为空,为空且设置了 O_NONBLOCK
标志的话就返回错误 EAGAIN
,否则就等待直到有连接到来。
1. 接收到SYN,发送SYN和ACK
tcp_v4_rcv // net/ipv4/af_inet.c:1684
|- tcp_v4_do_rcv // net/ipv4/tcp_ipv4.c:1822
|- tcp_rcv_state_process // net/ipv4/tcp_ipv4.c:1569
|- icsk->icsk_af_ops->conn_request() // net/ipv4/tcp_input.c:6051
对于 TCP/IPv4 来说,调用 icsk->icsk_af_ops->conn_request()
其实是调用 net/ipv4/tcp_ipv4.c
文件中的 tcp_v4_conn_request
:
tcp_v4_conn_request // net/ipv4/tcp_ipv4.c:1935
|- tcp_conn_request // net/ipv4/tcp_ipv4.c:1397
|- inet_reqsk_alloc // net/ipv4/tcp_input.c:6461
|- ireq->ireq_state = TCP_NEW_SYN_RECV // net/ipv4/tcp_input.c:6374
|- af_ops->send_synack() // net/ipv4/tcp_input.c:6556
在连接请求处理函数 tcp_conn_request
中,首先把新分配的套接字的 request_sock
设置为 TCP_NEW_SYN_RECV
状态。然后调用 af_ops->send_synack()
来发送 SYN 和 ACK 。对于 TCP/IPv4 来说,调用 af_ops->send_synack()
其实是调用 net/ipv4/tcp_ipv4.c
文件中的 tcp_v4_send_synack
。而函数 tcp_v4_send_synack
做的也确实是发送一个带有 SYN 和 ACK 标志的包。
2. 接收到ACK
tcp_v4_rcv // net/ipv4/af_inet.c:1684
|- tcp_check_req // net/ipv4/tcp_ipv4.c:1773
|- inet_csk(sk)->icsk_af_ops->syn_recv_sock() // net/ipv4/tcp_minisocks.c:789
|- inet_csk_complete_hashdance // net/ipv4/tcp_minisocks.c:797
|- tcp_child_process // net/ipv4/tcp_ipv4.c:1792
对于 TCP/IPv4 来说,调用
inet_csk(sk)->icsk_af_ops->syn_recv_sock()
其实是调用net/ipv4/tcp_ipv4.c
文件中的tcp_v4_syn_recv_sock
:tcp_v4_syn_recv_sock // net/ipv4/tcp_ipv4.c:1936 |- tcp_create_openreq_child // net/ipv4/tcp_ipv4.c:1429 |- inet_csk_clone_lock // net/ipv4/tcp_minisocks.c:452 |- inet_sk_set_state(newsk, TCP_SYN_RECV) // net/ipv4/inet_connection_sock.c:829
需要注意的是,新创建的套接字的状态为
TCP_SYN_RECV
。函数
inet_csk_complete_hashdance
主要做的是将请求控制块从未完成连接队列中删除,加入到已完成连接队列中:inet_csk_complete_hashdance // net/ipv4/tcp_minisocks.c:797 |- inet_csk_reqsk_queue_drop // net/ipv4/inet_connection_sock.c:992 |- reqsk_queue_unlink // net/ipv4/inet_connection_sock.c:703 |- inet_csk_reqsk_queue_add // net/ipv4/inet_connection_sock.c:994
函数
tcp_child_process
进一步对新创建的套接字进行处理并把状态更新为TCP_ESTABLISHED
:tcp_child_process // net/ipv4/tcp_ipv4.c:1792 |- tcp_rcv_state_process // net/ipv4/tcp_minisocks.c:851 |- tcp_set_state(sk, TCP_ESTABLISHED) // net/ipv4/tcp_input.c:6132
5. close
1. 发送FIN
tcp_close // net/ipv4/tcp_ipv4.c:2458
|- tcp_close_state // net/ipv4/tcp.c:2393
|- tcp_set_state(sk, ns) // net/ipv4/tcp.c:2294
|- tcp_send_fin // net/ipv4/tcp.c:2423
|- TCP_SKB_CB(tskb)->tcp_flags |= TCPHDR_FIN // net/ipv4/tcp_output.c:3087
|- sk_stream_wait_close // net/ipv4/tcp.c:2426
对于状态是 TCP_ESTABLISHED
的套接字,函数 tcp_close_state
会把套接字的状态更新为 TCP_FIN_WAIT1
。 tcp_send_fin
函数则发送带有 FIN 标记的包给对方。
2. 接收ACK
tcp_v4_rcv // net/ipv4/af_inet.c:1684
|- tcp_v4_do_rcv // net/ipv4/tcp_ipv4.c:1832
|- tcp_rcv_state_process // net/ipv4/tcp_ipv4.c:1569
|- tcp_set_state(sk, TCP_FIN_WAIT2) // net/ipv4/tcp_input.c:6175
3. 等待FIN并发送ACK
tcp_v4_rcv // net/ipv4/af_inet.c:1684
|- tcp_v4_do_rcv // net/ipv4/tcp_ipv4.c:1832
|- tcp_rcv_state_process // net/ipv4/tcp_ipv4.c:1569
|- tcp_data_queue // net/ipv4/tcp_input.c:6262
|- tcp_fin // net/ipv4/tcp_input.c:4756
|- tcp_send_ack(sk) // net/ipv4/tcp_input.c:4139
|- tcp_time_wait(TCP_TIME_WAIT) // net/ipv4/tcp_input.c:4140
|- inet_twsk_alloc // net/ipv4/tcp_minisocks.c:259
|- tw->tw_state = TCP_TIME_WAIT // net/ipv4/inet_timewait_sock.c:175
|- timer_setup(tw_timer_handler) // net/ipv4/inet_timewait_sock.c:188
|- inet_twsk_schedule() // net/ipv4/tcp_minisocks.c:319
在函数 tcp_fin
中,发送了 ACK 后,并进入 TCP_TIME_WAIT
状态。同时注册一个超时的定时器。
注意,虽然在
inet_twsk_alloc
函数中,修改的是tw->tw_state
字段为TCP_TIME_WAIT
状态。但是查看宏tw_state
的定义可以知道,tw_state
指向的依旧是结构体sock_common
中的字段skc_state
。
6. shutdown
调用流程如下:
__sys_shutdown // net/socket.c:1973
|- sock->ops->shutdown() // net/socket.c:1965
对于 TCP/IPv4 来说,调用 sock->ops->shutdown()
其实是调用 net/ipv4/tcp_ipv4.c
文件中的 tcp_shutdown
:
void tcp_shutdown(struct sock *sk, int how)
{
if (!(how & SEND_SHUTDOWN))
return;
/* If we've already sent a FIN, or it's a closed state, skip this. */
if ((1 << sk->sk_state) &
(TCPF_ESTABLISHED | TCPF_SYN_SENT |
TCPF_SYN_RECV | TCPF_CLOSE_WAIT)) {
/* Clear out any half completed packets. FIN if needed. */
if (tcp_close_state(sk))
tcp_send_fin(sk);
}
}
与调用 close
类似,
四、IP层及以下层的工作
1. IP层发送包
上层想要发送 IP 包,都会调用函数 ip_local_out
来发送。
ip_local_out // include/net/ip.h:165
|- dst_output // net/ipv4/ip_output.c:125
|- skb_dst(skb)->output() // include/net/dst.h:455
对 IP 协议来说, skb_dst(skb)->output()
实际调用的是 net/ipv4/ip_output.c
文件中的 ip_output
函数。
ip_output // net/ipv4/route.c:1636
|- ip_finish_output // net/ipv4/ip_output.c:408
|- ip_finish_output2 // net/ipv4/ip_output.c:318
|- neigh_output // net/ipv4/ip_output.c:230
|- neigh_hh_output // include/net/neighbour.h:499
|- dev_queue_xmit // include/net/neighbour.h:491
调用 dev_queue_xmit
是将 IP 包交给网络设备子系统来发送,而 dev_queue_xmit
直接调用的函数 __dev_queue_xmit
。
int dev_queue_xmit(struct sk_buff *skb)
{
return __dev_queue_xmit(skb, NULL);
}
static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
txq = netdev_pick_tx(dev, skb, sb_dev);
q = rcu_dereference_bh(txq->qdisc);
if (q->enqueue) {
__dev_xmit_skb(skb, q, dev, txq);
goto out;
}
/* The device has no queue. Common case for software devices:
* loopback, all the sorts of tunnels...
*/
if (dev->flags & IFF_UP) {
dev_hard_start_xmit(skb, dev, txq, &rc);
}
}
在函数 __dev_queue_xmit
内部:
针对有发送队列的设备
首先调用
netdev_pick_tx
选择一个发送队列(现在的网卡一般都支持多个发送队列),然后再调用__dev_xmit_skb
进行发送。在经过层层调用后,最后会调用的函数dev_hard_start_xmit
发送包。|- __dev_xmit_skb // net/core/dev.c:3807 |- q->enqueue(skb, q, &to_free) // net/core/dev.c:3450 |- qdisc_run // net/core/dev.c:3451 |- __qdisc_run // include/net/pkt_sched.h:120 |- qdisc_restart // net/sched/sch_generic.c:403 |- sch_direct_xmit // net/sched/sch_generic.c:395 |- dev_hard_start_xmit // net/sched/sch_generic.c:332
需要注意函数
__qdisc_run
的实现:void __qdisc_run(struct Qdisc *q) { int quota = dev_tx_weight; int packets; while (qdisc_restart(q, &packets)) { /* * Ordered by possible occurrence: Postpone processing if * 1. we've exceeded packet quota * 2. another process needs the CPU; */ quota -= packets; if (quota <= 0 || need_resched()) { __netif_schedule(q); break; } } }
如果系统态 CPU 发送网络包不够用的时候,会调用函数
__netif_schedule
进而调用函数__netif_reschedule
触发一个软中断NET_TX_SOFTIRQ
。而对应的中断处理函数net_tx_action
会重新调用qdisc_run
继续发送剩余的网络包。针对没有发送队列的设备
如注释里说的回环地址,隧道网络等,就直接调用
dev_hard_start_xmit
发送包。
所以,不管设备有无发送队列,最后都会调用函数 dev_hard_start_xmit
:
|- dev_hard_start_xmit // net/core/dev.c:3262
|- xmit_one // net/core/dev.c:3272
|- netdev_start_xmit // net/core/dev.c:3256
|- __netdev_start_xmit // include/linux/netdevice.h:4359
|- ops->ndo_start_xmit // include/linux/netdevice.h:4345
在经过层层调用后,最后调用的 ops->ndo_start_xmit()
其实是各个网卡驱动的实现了。
2. Intel的igb网卡驱动的发送流程
对网卡驱动 drivers/net/ethernet/intel/igb
来说, 函数 ops->ndo_start_xmit
调用的其实就是 igb_netdev_ops
中的 ndo_start_xmit
的值,即 drivers/net/ethernet/intel/igb/igb_main.c
文件中的 igb_xmit_frame
函数。
igb_xmit_frame // drivers/net/ethernet/intel/igb/igb_main.c:2865
|- igb_xmit_frame_ring // drivers/net/ethernet/intel/igb/igb_main.c:6201
|- igb_tx_map // drivers/net/ethernet/intel/igb/igb_main.c:6157
函数 igb_tx_map
做的主要的工作就是把网络包数据映射到 RAM 的 DMA 区域,然后唤醒设备发送数据。
3. Intel的igb网卡驱动的初始化
模块加载
igb_init_module // drivers/net/ethernet/intel/igb/igb_main.c:680 |- pci_register_driver(&igb_driver) // drivers/net/ethernet/intel/igb/igb_main.c:676 |- __pci_register_driver // include/linux/pci.h:1289 |- driver_register // drivers/pci/pci-driver.c:1409 |- bus_add_driver // drivers/base/driver.c:170 |- driver_attach // drivers/base/bus.c:672 |- __driver_attach // drivers/base/dd.c:922 |- driver_probe_device // drivers/base/dd.c:903 |- really_probe // drivers/base/dd.c:667 |- drv->probe(dev) // drivers/base/dd.c:510
最后调用的
drv->probe(dev)
其实就是igb_driver
中的probe
的值,即drivers/net/ethernet/intel/igb/igb_main.c
文件中的igb_probe
函数。igb_probe // drivers/net/ethernet/intel/igb/igb_main.c:230 |- netdev->netdev_ops = &igb_netdev_ops // drivers/net/ethernet/intel/igb/igb_main.c:3095 |- igb_sw_init // drivers/net/ethernet/intel/igb/igb_main.c:3121 |- igb_init_interrupt_scheme // drivers/net/ethernet/intel/igb/igb_main.c:3888 |- igb_alloc_q_vectors // drivers/net/ethernet/intel/igb/igb_main.c:1387 |- igb_alloc_q_vector // drivers/net/ethernet/intel/igb/igb_main.c:1331 |- netif_napi_add(igb_poll) // drivers/net/ethernet/intel/igb/igb_main.c:1216 |- napi->poll = poll // net/core/dev.c:6194
对 Intel 的 igb 网卡来说,模块加载函数
igb_probe
做了:- 初始化了设备相关的操作回调函数信息
igb_netdev_ops
。 - 注册了 napi 的轮询回调函数
igb_poll
。
- 初始化了设备相关的操作回调函数信息
启动设备
在函数
igb_probe
中,通过语句netdev->netdev_ops = &igb_netdev_ops
初始化了网卡相关的操作回调。在网卡启用时,比如执行命令ifconfig eth0 up
时,系统会依次调用:dev_open // net/core/dev.c:1430 |- __dev_open // net/core/dev.c:1437 |- ops->ndo_open(dev) // net/core/dev.c:1402
对于网卡驱动
drivers/net/ethernet/intel/igb
来说,函数ops->ndo_open(dev)
调用的就是函数igb_open
。igb_open // drivers/net/ethernet/intel/igb/igb_main.c:2863 |- __igb_open // drivers/net/ethernet/intel/igb/igb_main.c:4017 |- igb_request_irq // drivers/net/ethernet/intel/igb/igb_main.c:3953 |- igb_request_msix // drivers/net/ethernet/intel/igb/igb_main.c:1416 |- request_irq(igb_msix_ring) // drivers/net/ethernet/intel/igb/igb_main.c:968 |- igb_irq_enable(adapter) // drivers/net/ethernet/intel/igb/igb_main.c:3978
在
igb_open
函数调用的深处,对于多队列网卡,通过request_irq(igb_msix_ring)
为每个队列都注册了中断处理函数igb_msix_ring
。设置好中断处理程序后,再调用igb_irq_enable(adapter)
来开启中断。
4. Intel的igb网卡驱动的接收流程
当网卡的某个队列中收到网络数据后,会触发一个中断,这个中断的处理函数就是在启动设备时设置的 igb_msix_ring
函数:
igb_msix_ring // drivers/net/ethernet/intel/igb/igb_main.c:6621
|- napi_schedule // drivers/net/ethernet/intel/igb/igb_main.c:6628
|- __napi_schedule // include/linux/netdevice.h:445
|- ____napi_schedule // net/core/dev.c:5892
网卡队列中断处理函数最深处的调用 ____napi_schedule
主要做两件事:
- 保存待处理的数据。
- 触发中断
NET_RX_SOFTIRQ
。
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
而中断 NET_RX_SOFTIRQ
的处理函数是 net_rx_action
:
net_rx_action // net/core/dev.c:9874
|- napi_poll // net/core/dev.c:6338
|- n->poll() // net/core/dev.c:6272
中断处理函数 net_rx_action
,最终的会调用函数 n->poll()
。这函数就是网卡驱动模块加载时注册的处理回调函数 igb_poll
:
igb_poll // drivers/net/ethernet/intel/igb/igb_main.c:1216
|- igb_clean_rx_irq // drivers/net/ethernet/intel/igb/igb_main.c:7759
|- napi_gro_receive // drivers/net/ethernet/intel/igb/igb_main.c:8408
|- napi_skb_finish // net/core/dev.c:5631
|- netif_receive_skb_internal // net/core/dev.c:5600
|- __netif_receive_skb // net/core/dev.c:5156
|- __netif_receive_skb_one_core // net/core/dev.c:5066
|- __netif_receive_skb_core // net/core/dev.c:4952
在函数 __netif_receive_skb_core
中:
static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
struct packet_type **ppt_prev)
{
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
&ptype_base[ntohs(type) &
PTYPE_HASH_MASK]);
}
第一个循环 list_for_each_entry_rcu(ptype, &ptype_all, list)
中的 ptype_all
是一个设备无关的保存有所有协议处理回调的地方,而第二个循环 list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list)
中的 skb->dev->ptype_all
是一个设备有关的保存有所有协议处理回调的地方。这两个循环就能让像 tcpdump 这样的程序监听处理到收到的所有的网络包。
后面代码 deliver_ptype_list_skb
中的 ptype_base
是保存所有支持的3层协议处理函数的地方。才是将包转发给对应L3处理函数。函数 deliver_ptype_list_skb
的调用流程如下:
deliver_ptype_list_skb // net/core/dev.c:4905
|- deliver_skb // net/core/dev.c:1971
|- pt_prev->func() // net/core/dev.c:1956
对于 IP 协议来说,其注册到 ptype_base
的流程如下:
inet_init // net/ipv4/af_inet.c:1890
|- dev_add_pack(&ip_packet_type) // net/core/dev.c:408
dev_add_pack(&ip_packet_type)
函数内部就是把 ip_packet_type
添加到处理 ETH_P_IP
的协议链中。
static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
.list_func = ip_list_rcv,
};
5. 接收IP包
所以对 IP 来说 pt_prev->func()
实际调用的是 ip_rcv
函数。
ip_rcv // ip_packet_type:1886
|- ip_rcv_finish // net/ipv4/ip_input.c:526
|- dst_input // net/ipv4/ip_input.c:414
|- skb_dst(skb)->input(skb) // include/net/dst.h:461
对 TCP/IPv4 来说 skb_dst(skb)->input(skb)
调用的其实是 net/ipv4/ip_input.c
文件中的 ip_local_deliver
。
ip_local_deliver // net/ipv4/route.c:1638
|- ip_local_deliver_finish // net/ipv4/ip_input.c:258
|- ipprot->handler(skb) // net/ipv4/ip_input.c:215
这里调用的 ipprot->handler(skb)
就是传输层的处理回调了。 在文件 net/ipv4/af_inet.c
中的函数 inet_init
内初始化:
static int __init inet_init(void)
{
inet_add_protocol(&icmp_protocol, IPPROTO_ICMP);
inet_add_protocol(&udp_protocol, IPPROTO_UDP);
inet_add_protocol(&tcp_protocol, IPPROTO_TCP);
inet_add_protocol(&igmp_protocol, IPPROTO_IGMP)
}
五、参考资料
Linux syscall过程分析(万字长文)
Linux 内核如何处理系统调用
Monitoring and Tuning the Linux Networking Stack: Sending Data
Monitoring and Tuning the Linux Networking Stack: Receiving Data