TCP/IP协议栈在Linux内核中的运行时序分析
1.网络分层模型
OSI模型,即开放式通信系统互联参考模型(Open System Interconnection Reference Model),是国际标准化组织(ISO)提出的一个试图使各种计算机在世界范围内互连为网络的标准框架。它提供给开发者一个必须的、通用的概念以便开发完善,可以用来解释连接不同系统的框架。
TCP/IP协议(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。与OSI模型不同,TCP/IP模型并不仅是理论上的网络标准,而是现有的事实模型,它在一定程度上参考了OSI模型,并根据当时的现存网络发展而来。
OSI模型及TCP/IP协议如下图所示:
物理层:规定了激活、维持、关闭通信端点之间的机械特性、电气特性、功能特性及过程特性。该层为上层协议提供了一个传输数据的物理媒体。在这一层,数据的单位称为比特(bit)。该层的典型规范代表包括:EIA/TIARS-232、EIA/TIARS-449、V.35等。
数据链路层:在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。该层的典型规范代表包括:SDLC、HDLC、PPP、STP、帧中继等。
网络层:负责对子网间的数据包进行路由选择。网络层还可以实现拥塞控制、网际互联等功能。在这一层,数据的单位称为数据包(packet)。该层的典型规范代表包括:IP、IPX、RIP、OSPF、ARP、RARP、ICMP、IGMP等。
传输层:是第一个端到端,即主机到主机的层次。传输层负责将上层数据分段并提供端到端的、可靠的或不可靠的传输。此外,传输层还要处理端到端的差错控制和流量控制问题。在这一层,数据的单位称为数据段(segment)。该层的典型规范代表包括:TCP、UDP、SPX等。
会话层:管理主机之间的会话进程,即负责建立、管理、终止进程之间的会话。会话层还利用在数据中插入校验点来实现数据的同步。
表示层:对上层数据或信息进行变换以保证一个主机应用层信息可以被另一个主机的应用程序理解。表示层的数据转换包括数据的加密、压缩、格式转换等。
应用层:为操作系统或网络应用程序提供访问网络服务的接口。该层的典型规范代表包括:Telnet、FTP、HTTP、SNMP等。
2.Linux中的网络模型
Linux网络子系统提供了对各种网络标准的存取和各种硬件的支持,其可以分为插口层,协议层和接口层,整体结构如图所示:
BSD Socket层:这一部分处理BSD socket相关操作,每个socket在内核中以struct socket结构体现,这一部分的文件主要有:/net/socket.c、/net/protocol.c等。
INET socket层:BSD socket是个可以用于各种网络协议的接口,而当用于tcp/ip,即建立了AF_INET形式的socket时,还需要保留些额外的参数,于是就有了struct sock结构。文件主要有:/net/ipv4/protocol.c、/net/ipv4/af_inet.c、/net/core/sock.c等。
TCP/UDP层:处理传输层的操作,传输层用struct inet_protocol和struct proto两个结构表示,文件主要有:/net/ipv4/udp.c、/net/ipv4/datagram.c、/net/ipv4/tcp.c等。
IP层:处理网络层的操作,网络层用struct packet_type结构表示,文件主要有:/net/ipv4/ip_forward.c、ip_fragment.c、ip_input.c等。
数据链路层和驱动程序:每个网络设备以struct net_device表示,通用的处理在dev.c中,驱动程序都在/driver/net目录下。
各层次中涉及的主要函数及数据结构如图所示:
3.SOCKET简介
linux中的网络编程通过socket接口实现,socket既是一种特殊的IO,也是一种文件描述符。socket是通过IP+端口号进行匹配的,匹配之后可以通过socket进行数据的发送与接收(socket本质是文件描述符fd)。具体流程(TCP)如下:
3.1 socket创建
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
domain
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是采用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type
(1)SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
(2)SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
(3)SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
(4)SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)。
(5)SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。
protocol
0:默认协议
返回值
成功返回一个新的文件描述符(也叫监听套接字),失败返回-1。
3.2 bind绑定
在创建了套接字之后需要IP和端口号和套接字绑定在一起( IP地址:在网络环境中,唯一标识一台主机,端口号:在主机中唯一标识一个进程)。struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:文件描述符;
addr:构造出IP地址加端口号;
addrlen:sizeof(addr)长度;
返回值:成功返回0,失败返回-1,设置errno。
3.3 listen
创建了套接字之后通常需要等待客户端的连接,此时可以使用listen函数将该套接字转换为倾听套接字;可以指定同时连接的最大客户端数量;若达到数量上限,新客户端等待其它已链接的客户端链接结束。
#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog);
sockfd:文件描述符;
backlog:排队建立3次握手队列和刚刚建立3次握手队列的连接数和;
返回值:成功返回0,失败返回-1。
3.4 accept
当服务器倾听到一个连接之后,可以使用函数accept从倾听套接字的完成连接队列中接收一个连接,如果这个完成连接队列为空,则会使得这个进程进入睡眠状态。
#include <sys/types.h> #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:文件描述符;
addr:传出参数,返回链接客户端地址信息,含IP地址和端口号;
addrlen:传入传出参数,传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小;
返回值:成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno。
3.5 connect客户端连接函数
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:文件描述符;
addr:传入参数,指定服务器端地址信息,含IP地址和端口号;
addrlen:传入参数,传入sizeof(addr)大小;
返回值:成功返回0,失败返回-1,设置errno。
3.6 读写函数及关闭函数
#include <unistd.h> int read(int fd, char *buf, int len); int write(int fd, char *buf, int len); int close(int fd);
fd:套接字描述符;
buf:指定数据缓冲区;
len:指定接收或发送的数据量大小(以字节为单位);
返回值:返回读/写成功的数据量大小,失败则返回-1。
3.7socket编程实例
利用socket套接字进行网络编程,其实现结果如图所示:
4.应用层
4.1应用层发送数据过程简介
1.网络应用调用Socket API socket (int family, int type, int protocol) 创建一个 socket,该调用最终会调用 Linux system call socket() ,并最终调用 Linux Kernel 的 sock_create() 方法。该方法返回被创建好了的那个 socket 的 file descriptor。对于每一个 userspace 网络应用创建的 socket,在内核中都有一个对应的 struct socket和 struct sock。其中,struct sock 有三个队列(queue),分别是 rx , tx 和 err,在 sock 结构被初始化的时候,这些缓冲队列也被初始化完成;在收据收发过程中,每个 queue 中保存要发送或者接受的每个 packet 对应的 Linux 网络栈 sk_buffer 数据结构的实例 skb。
2.对于TCP socket 来说,应用调用 connect()API ,使得客户端和服务器端通过该 socket 建立一个虚拟连接。在此过程中,TCP 协议栈通过三次握手会建立 TCP 连接。默认地,该 API 会等到 TCP 握手完成连接建立后才返回。在建立连接的过程中的一个重要步骤是,确定双方使用的 Maxium Segemet Size (MSS)。因为 UDP 是面向无连接的协议,因此它是不需要该步骤的。
3.应用调用 Linux Socket 的 send 或者 write API 来发出一个 message 给接收端。
4.sock_sendmsg 被调用,它使用 socket descriptor 获取 sock struct,创建 message header 和 socket control message。
5._sock_sendmsg 被调用,根据 socket 的协议类型,调用相应协议的发送函数。
6.对于TCP ,调用 tcp_sendmsg 函数。
7.对于UDP 来说,userspace 应用可以调用 send()/sendto()/sendmsg() 三个 system call 中的任意一个来发送 UDP message,它们最终都会调用内核中的 udp_sendmsg() 函数。
4.2应用层接收数据过程简介
1.每当用户应用调用 read 或者 recvfrom 时,该调用会被映射为/net/socket.c 中的 sys_recv 系统调用,并被转化为 sys_recvfrom 调用,然后调用 sock_recgmsg 函数。
2.对于 INET 类型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法会被调用,它会调用相关协议的数据接收方法。
3.对TCP 来说,调用 tcp_recvmsg。该函数从 socket buffer 中拷贝数据到 user buffer。
4.对UDP 来说,从 user space 中可以调用三个 system call recv()/recvfrom()/recvmsg() 中的任意一个来接收 UDP package,这些系统调用最终都会调用内核中的 udp_recvmsg 方法。
4.3利用gdb进行调试
5.传输层
传输层的最终目的是向它的用户提供高效的、可靠的和成本有效的数据传输服务,主要功能包括:
(1)构造TCP segment
(2)计算checksum
(3)发送回复包(ACK)
(4)滑动窗口协议
TCP协议栈的大致流程如图所示:
5.1TCP栈发送数据过程简介
1.tcp_sendmsg 函数会首先检查已经建立的 TCP connection 的状态,然后获取该连接的 MSS,开始 segement 发送流程。
2.构造TCP 段的 playload:它在内核空间中创建该 packet 的 sk_buffer 数据结构的实例 skb,从 userspace buffer 中拷贝 packet 的数据到 skb 的 buffer。
3.构造TCP header。
4.计算TCP 校验和(checksum)和 顺序号 (sequence number):TCP校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。TCP校验和覆盖 TCP 首部和 TCP 数据;TCP的校验和是必需的。
5.发到 IP 层处理:调用 IP handler 句柄 ip_queue_xmit,将 skb 传入 IP 处理流程。
5.2TCP栈接收数据过程简介
1.传输层TCP 处理入口在 tcp_v4_rcv 函数(位于 linux/net/ipv4/tcp ipv4.c 文件中),它会做 TCP header 检查等处理。
2.调用 _tcp_v4_lookup,查找该package的open socket。如果找不到,该package会被丢弃。接下来检查 socket 和 connection 的状态。
3.如果socket 和 connection 一切正常,调用 tcp_prequeue 使 package 从内核进入 user space,放进 socket 的 receive queue。然后 socket 会被唤醒,调用 system call,并最终调用 tcp_recvmsg 函数去从 socket recieve queue 中获取 segment。
5.3TCP栈相关代码分析
send和recv是TCP常用的发送数据和接受数据函数:
ssize_t recv(int sockfd, void *buf, size_t len, int flags) ssize_t send(int sockfd, const void *buf, size_t len, int flags)
首先,当调用send()函数时,内核封装send()为sendto(),然后发起系统调用。其实也很好理解,send()就是sendto()的一种特殊情况,而sendto()在内核的系统调用服务程序为sys_sendto。
int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags, struct sockaddr __user *addr, int addr_len) { struct socket *sock; struct sockaddr_storage address; int err; struct msghdr msg; struct iovec iov; int fput_needed; err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter); if (unlikely(err)) return err; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (!sock) goto out; msg.msg_name = NULL; msg.msg_control = NULL; msg.msg_controllen = 0; msg.msg_namelen = 0; if (addr) { err = move_addr_to_kernel(addr, addr_len, &address); if (err < 0) goto out_put; msg.msg_name = (struct sockaddr *)&address; msg.msg_namelen = addr_len; } if (sock->file->f_flags & O_NONBLOCK) flags |= MSG_DONTWAIT; msg.msg_flags = flags; err = sock_sendmsg(sock, &msg); out_put: fput_light(sock->file, fput_needed); out: return err; }
这里定义了一个struct msghdr msg,它是用来表示要发送的数据的一些属性。
struct msghdr { void *msg_name; /* 接收方的struct sockaddr结构体地址 (用于udp)*/ int msg_namelen; /* 接收方的struct sockaddr结构体地址(用于udp)*/ struct iov_iter msg_iter; /* io缓冲区的地址 */ void *msg_control; /* 辅助数据的地址 */ __kernel_size_t msg_controllen; /* 辅助数据的长度 */ unsigned int msg_flags; /*接受消息的表示 */ struct kiocb *msg_iocb; /* ptr to iocb for async requests */ };
还有一个struct iovec,它被称为io向量,用来表示io数据的一些信息。
struct iovec { void __user *iov_base; /* 要传输数据的用户态下的地址*) */ __kernel_size_t iov_len; /*要传输数据的长度 */ };
所以,__sys_sendto函数其实做了3件事:1.通过fd获取了对应的struct socket。2.创建了用来描述要发送的数据的结构体struct msghdr。3.调用了sock_sendmsg来执行实际的发送。继续追踪这个函数,会看到最终调用的是sock->ops->sendmsg(sock,msg,msg_data_left(msg));,即socket在初始化时复制给结构体struct proto tcp_prot的函数tcp_sendmsg。
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, .keepalive = tcp_set_keepalive, .recvmsg = tcp_recvmsg, .sendmsg = tcp_sendmsg, ...... }
tcp_sendmsg实际上调用的是int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)。
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size) { struct tcp_sock *tp = tcp_sk(sk);/*进行了强制类型转换*/ struct sk_buff *skb; flags = msg->msg_flags; ...... if (copied) tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH, size_goal); }
在tcp_sendmsg_locked中,完成的是将所有的数据组织成发送队列,这个发送队列是struct sock结构中的一个域sk_write_queue,这个队列的每一个元素是一个skb,里面存放的就是待发送的数据。然后调用了tcp_push()函数。
struct sock{ ... struct sk_buff_head sk_write_queue;/*指向skb队列的第一个元素*/ ... struct sk_buff *sk_send_head;/*指向队列第一个还没有发送的元素*/ }
在tcp协议的头部有几个标志字段:URG、ACK、RSH、RST、SYN、FIN,tcp_push中会判断这个skb的元素是否需要push,如果需要就将tcp头部字段的push置一,置一的过程如下:
static void tcp_push(struct sock *sk, int flags, int mss_now, int nonagle, int size_goal) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; skb = tcp_write_queue_tail(sk); if (!skb) return; if (!(flags & MSG_MORE) || forced_push(tp)) tcp_mark_push(tp, skb); tcp_mark_urg(tp, flags); if (tcp_should_autocork(sk, skb, size_goal)) { /* avoid atomic op if TSQ_THROTTLED bit is already set */ if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) { NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING); set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags); } /* It is possible TX completion already happened * before we set TSQ_THROTTLED. */ if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize) return; } if (flags & MSG_MORE) nonagle = TCP_NAGLE_CORK; __tcp_push_pending_frames(sk, mss_now, nonagle); }
首先struct tcp_skb_cb结构体存放的就是tcp的头部,头部的控制位为tcp_flags,通过tcp_mark_push会将skb中的cb,也就是48个字节的数组,类型转换为struct tcp_skb_cb,这样位于skb的cb就成了tcp的头部。
static inline void tcp_mark_push(struct tcp_sock *tp, struct sk_buff *skb) { TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; tp->pushed_seq = tp->write_seq; } ... #define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0])) ... struct sk_buff { ... char cb[48] __aligned(8); ... }
struct tcp_skb_cb { __u32 seq; /* Starting sequence number */ __u32 end_seq; /* SEQ + FIN + SYN + datalen */ __u8 tcp_flags; /* tcp头部标志,位于第13个字节tcp[13]) */ ...... };
然后,tcp_push调用__tcp_push_pending_frames(sk, mss_now, nonagle);函数发送数据:
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle) { if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_mask(sk, GFP_ATOMIC))) tcp_check_probe_timer(sk); }
随后又调用了tcp_write_xmit来发送数据:
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; unsigned int tso_segs, sent_pkts; int cwnd_quota; int result; bool is_cwnd_limited = false, is_rwnd_limited = false; u32 max_segs; /*统计已发送的报文总数*/ sent_pkts = 0; ...... /*若发送队列未满,则准备发送报文*/ while ((skb = tcp_send_head(sk))) { unsigned int limit; if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) { /* "skb_mstamp_ns" is used as a start point for the retransmit timer */ skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache; list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue); tcp_init_tso_segs(skb, mss_now); goto repair; /* Skip network transmission */ } if (tcp_pacing_check(sk)) break; tso_segs = tcp_init_tso_segs(skb, mss_now); BUG_ON(!tso_segs); /*检查发送窗口的大小*/ cwnd_quota = tcp_cwnd_test(tp, skb); if (!cwnd_quota) { if (push_one == 2) /* Force out a loss probe pkt. */ cwnd_quota = 1; else break; } if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) { is_rwnd_limited = true; break; ...... limit = mss_now; if (tso_segs > 1 && !tcp_urg_mode(tp)) limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle); if (skb->len > limit && unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE, skb, limit, mss_now, gfp))) break; if (tcp_small_queue_check(sk, skb, 0)) break; if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) break; ...... }
tcp_write_xmit位于tcpoutput.c中,它实现了tcp的拥塞控制,然后调用了tcp_transmit_skb(sk, skb, 1, gfp)传输数据,实际上调用的是__tcp_transmit_skb。
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt) { skb_push(skb, tcp_header_size); skb_reset_transport_header(skb); ...... /* 构建TCP头部和校验和 */ th = (struct tcphdr *)skb->data; th->source = inet->inet_sport; th->dest = inet->inet_dport; th->seq = htonl(tcb->seq); th->ack_seq = htonl(rcv_nxt); tcp_options_write((__be32 *)(th + 1), tp, &opts); skb_shinfo(skb)->gso_type = sk->sk_gso_type; if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) { th->window = htons(tcp_select_window(sk)); tcp_ecn_send(sk, skb, th, tcp_header_size); } else { /* RFC1323: The window in SYN & SYN/ACK segments * is never scaled. */ th->window = htons(min(tp->rcv_wnd, 65535U)); } ...... icsk->icsk_af_ops->send_check(sk, skb); if (likely(tcb->tcp_flags & TCPHDR_ACK)) tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt); if (skb->len != tcp_header_size) { tcp_event_data_sent(tp, sk); tp->data_segs_out += tcp_skb_pcount(skb); tp->bytes_sent += skb->len - tcp_header_size; } if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq) TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS, tcp_skb_pcount(skb)); tp->segs_out += tcp_skb_pcount(skb); /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */ skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb); skb_shinfo(skb)->gso_size = tcp_skb_mss(skb); /* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */ /* Cleanup our debris for IP stacks */ memset(skb->cb, 0, max(sizeof(struct inet_skb_parm), sizeof(struct inet6_skb_parm))); err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); ...... }
tcp_transmit_skb是tcp发送数据位于传输层的最后一步,这里首先对TCP数据段的头部进行了处理,然后调用了网络层提供的发送接口icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.f1);实现了数据的发送,自此,数据离开了传输层,传输层的任务也就结束了。
对于recv函数,与send类似,自然也是recvfrom的特殊情况,调用的也就是__sys_recvfrom,整个函数的调用路径与send非常类似:
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags, struct sockaddr __user *addr, int __user *addr_len) { ...... err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter); if (unlikely(err)) return err; sock = sockfd_lookup_light(fd, &err, &fput_needed); ..... msg.msg_control = NULL; msg.msg_controllen = 0; /* Save some cycles and don't copy the address if not needed */ msg.msg_name = addr ? (struct sockaddr *)&address : NULL; /* We assume all kernel code knows the size of sockaddr_storage */ msg.msg_namelen = 0; msg.msg_iocb = NULL; msg.msg_flags = 0; if (sock->file->f_flags & O_NONBLOCK) flags |= MSG_DONTWAIT; err = sock_recvmsg(sock, &msg, flags); if (err >= 0 && addr != NULL) { err2 = move_addr_to_user(&address, msg.msg_namelen, addr, addr_len); ..... }
__sys_recvfrom调用了sock_recvmsg来接收数据,整个函数实际调用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);,同样,根据tcp_prot结构的初始化,调用的其实是tcp_rcvmsg。接受函数比发送函数要复杂得多,因为数据接收不仅仅只是接收,tcp的三次握手也是在接收函数实现的,所以收到数据后要判断当前的状态,是否正在建立连接等,根据发来的信息考虑状态是否要改变,在这里,我们仅仅考虑在连接建立后数据的接收。
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len) { ...... if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) && (sk->sk_state == TCP_ESTABLISHED)) sk_busy_loop(sk, nonblock); lock_sock(sk); ..... if (unlikely(tp->repair)) { err = -EPERM; if (!(flags & MSG_PEEK)) goto out; if (tp->repair_queue == TCP_SEND_QUEUE) goto recv_sndq; err = -EINVAL; if (tp->repair_queue == TCP_NO_QUEUE) goto out; ...... last = skb_peek_tail(&sk->sk_receive_queue); skb_queue_walk(&sk->sk_receive_queue, skb) { last = skb; ...... if (!(flags & MSG_TRUNC)) { err = skb_copy_datagram_msg(skb, offset, msg, used); if (err) { /* Exception. Bailout! */ if (!copied) copied = -EFAULT; break; } } *seq += used; copied += used; len -= used; tcp_rcv_space_adjust(sk); ...... }
这里共维护了三个队列:prequeue、backlog、receive_queue、分别为预处理队列,后备队列和接收队列,在连接建立后,若没有数据到来,接收队列为空,进程会在sk_busy_loop函数内循环等待,知道接收队列不为空,并调用函数skb_copy_datagram_msg将接收到的数据拷贝到用户态,实际调用的是__skb_datagram_iter,这里同样用了struct msghdr *msg来实现。
int __skb_datagram_iter(const struct sk_buff *skb, int offset, struct iov_iter *to, int len, bool fault_short, size_t (*cb)(const void *, size_t, void *, struct iov_iter *), void *data) { int start = skb_headlen(skb); int i, copy = start - offset, start_off = offset, n; struct sk_buff *frag_iter; /* 拷贝tcp头部 */ if (copy > 0) { if (copy > len) copy = len; n = cb(skb->data + offset, copy, data, to); offset += n; if (n != copy) goto short_copy; if ((len -= copy) == 0) return 0; } /* 拷贝数据部分 */ for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) { int end; const skb_frag_t *frag = &skb_shinfo(skb)->frags[i]; WARN_ON(start > offset + len); end = start + skb_frag_size(frag); if ((copy = end - offset) > 0) { struct page *page = skb_frag_page(frag); u8 *vaddr = kmap(page); if (copy > len) copy = len; n = cb(vaddr + frag->page_offset + offset - start, copy, data, to); kunmap(page); offset += n; if (n != copy) goto short_copy; if (!(len -= copy)) return 0; } start = end; } }
拷贝完成后,函数返回,整个接收的过程也就完成了。
5.4利用gdb进行调试
5.5TCP三次握手分析
首先,客户端开始的时候,首先创建sock文件描述符,接着就进行connect发起连接服务器请求,阻塞等待服务器应答。
接着,服务器开始的时候,分配一个listen_sock文件描述符,接着进行bind绑定,绑定完毕之后进行listen监听,最后进行accept,此时阻塞等待客户端的连接。连接建立accept返回之后,分配一个新的文件描述符与客户端通信。
第一次握手:Client先产生一个初始序列号seq:8000,SYN标志位置1,将该数据包发送给Server端,之后Client端进入SYN_SENT状态,等待Client确认。
第二次握手:Server收到数据包后也发送自己的SYN报文作为响应,并初始化序列号seq=15000,为了确认Client的seq,Server将Client发送的seq加1作为ACK发送给Client,Server进入SYN_RCVD状态。
第三次握手:为了确认Server的SYN,Client将Server发送的seq加1作为ACK发送给Server。Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
通过这样的三次握手,客户端和服务端建立起可靠的全双工的连接,开始传送数据。三次握手的最主要目的是保证连接是全双工的,可靠更多的是通过重传机制来保证的。
5.6三次握手相关代码分析
从tcp_rcv函数开始,这个函数是一个分发器。当接收到一个tcp包的时候,底层就会调这个函数交给tcp层处理。
// daddr,saddr是ip头的字段,len为tcp头+数据长度 int tcp_rcv( struct sk_buff *skb, struct device *dev, struct options *opt, unsigned long daddr, unsigned short len, unsigned long saddr, int redo, struct inet_protocol * protocol ){ // tcp报文头 th = skb->h.th; // 从tcp的socket哈希链表中找到对应的socket结构 sk = get_sock(&tcp_prot, th->dest, saddr, th->source, daddr); // 读缓冲区已满,丢弃数据包 if (sk->rmem_alloc + skb->mem_len >= sk->rcvbuf) { kfree_skb(skb, FREE_READ); release_sock(sk); return(0); } // 这个就是上篇我们说的skb中关联的sock结构体。 skb->sk=sk; // 增加读缓冲区已使用的内存的大小 sk->rmem_alloc += skb->mem_len; // 连接还没有建立,不是通信数据包 if(sk->state!=TCP_ESTABLISHED) { // 是监听socket则可能是一个syn包 if(sk->state==TCP_LISTEN) { // 不存在这种可能的各种情况,直接丢包 if(th->rst || !th->syn || th->ack || ip_chk_addr(daddr)!=IS_MYADDR) { kfree_skb(skb, FREE_READ); release_sock(sk); return 0; } // 是个syn包,建立连接 tcp_conn_request(sk, skb, daddr, saddr, opt, dev, tcp_init_seq()); release_sock(sk); return 0; } } }
上面的函数主要的逻辑有两个:
1.get_sock是根据源ip、端口和目的ip、端口从tcp的sock_array哈希表里找到对应的sock结构体。
/* num 目的端口 raddr 源ip rnum 源端口 laddr 目的ip */ struct sock *get_sock(struct proto *prot, unsigned short num, unsigned long raddr, unsigned short rnum, unsigned long laddr) { struct sock *s; struct sock *result = NULL; int badness = -1; unsigned short hnum; hnum = ntohs(num); for(s = prot->sock_array[hnum & (SOCK_ARRAY_SIZE - 1)]; s != NULL; s = s->next) { int score = 0; // 绑定的端口是否等于报文中的目的端口 if (s->num != hnum) continue; // 该sock已经不用了,下一个 if(s->dead && (s->state == TCP_CLOSE)) continue; /* local address matches? */ // 绑定的ip,bind的时候赋值 if (s->saddr) { // 报文中的目的ip是不是等于该socket绑定的ip if (s->saddr != laddr) continue; score++; } /* remote address matches? */ // 目的ip if (s->daddr) { if (s->daddr != raddr) continue; score++; } /* remote port matches? */ // 目的端口 if (s->dummy_th.dest) { if (s->dummy_th.dest != rnum) continue; score++; } /* perfect match? */ // 全匹配,直接返回 if (score == 3) return s; /* no, check if this is the best so far.. */ if (score <= badness) continue; // 记录最好的匹配项 result = s; badness = score; } return result; }
对于监听型的socket,我们在bind的时候写入了绑定的ip和端口。对于监听型的socket,是没有目的ip和目的端口的。通信型的socket才有。所以上面的函数根据服务端绑定的ip和端口。判断是否等于tcp报文中的目的ip和端口。最后拿到监听型的sock结构体。
2.tcp_conn_request处理sync包,tcp_conn_request里完成了tcp的第一次和第二次握手。
// 收到一个syn包时的处理 static void tcp_conn_request(struct sock *sk, struct sk_buff *skb, unsigned long daddr, unsigned long saddr, struct options *opt, struct device *dev, unsigned long seq) { struct sk_buff *buff; struct tcphdr *t1; unsigned char *ptr; struct sock *newsk; struct tcphdr *th; struct device *ndev=NULL; int tmp; struct rtable *rt; th = skb->h.th; // 过载则丢包,防止ddos,max_ack_backlog即listen的参数 if (sk->ack_backlog >= sk->max_ack_backlog) { tcp_statistics.TcpAttemptFails++; kfree_skb(skb, FREE_READ); return; } // 分配一个新的sock结构用于通信。accept的时候返回的就是这个sock结构体 newsk = (struct sock *) kmalloc(sizeof(struct sock), GFP_ATOMIC); // 从listen套接字复制内容,再覆盖某些字段 memcpy(newsk, sk, sizeof(*newsk)); skb_queue_head_init(&newsk->write_queue); skb_queue_head_init(&newsk->receive_queue); newsk->send_head = NULL; newsk->send_tail = NULL; skb_queue_head_init(&newsk->back_log); newsk->rtt = 0; /*TCP_CONNECT_TIME<<3*/ newsk->rto = TCP_TIMEOUT_INIT; newsk->mdev = 0; newsk->max_window = 0; newsk->cong_window = 1; newsk->cong_count = 0; newsk->ssthresh = 0; newsk->backoff = 0; newsk->blog = 0; newsk->intr = 0; newsk->proc = 0; newsk->done = 0; newsk->partial = NULL; newsk->pair = NULL; newsk->wmem_alloc = 0; newsk->rmem_alloc = 0; newsk->localroute = sk->localroute; newsk->max_unacked = MAX_WINDOW - TCP_WINDOW_DIFF; newsk->err = 0; newsk->shutdown = 0; newsk->ack_backlog = 0; // 期待收到的对端下一个字节的序列号 newsk->acked_seq = skb->h.th->seq+1; // 进程可以读但是还没有读取的字节序列号 newsk->copied_seq = skb->h.th->seq+1; // 当收到对端fin包的时候,回复的ack包中的序列号 newsk->fin_seq = skb->h.th->seq; // 进入syn_recv状态 newsk->state = TCP_SYN_RECV; newsk->timeout = 0; newsk->ip_xmit_timeout = 0; // 下一个发送的字节的序列号 newsk->write_seq = seq; // 可发送的字节序列号最大值 newsk->window_seq = newsk->write_seq; // 序列号小于rcv_ack_seq的数据包都已经收到 newsk->rcv_ack_seq = newsk->write_seq; newsk->urg_data = 0; newsk->retransmits = 0; // 关闭套接字的时候不需要等待一段时间才能关闭 newsk->linger=0; newsk->destroy = 0; init_timer(&newsk->timer); newsk->timer.data = (unsigned long)newsk; newsk->timer.function = &net_timer; init_timer(&newsk->retransmit_timer); newsk->retransmit_timer.data = (unsigned long)newsk; newsk->retransmit_timer.function=&retransmit_timer; // 记录端口,发送ack和get_sock的时候用 newsk->dummy_th.source = skb->h.th->dest; newsk->dummy_th.dest = skb->h.th->source; // 记录ip,发送ack和get_sock的时候用 newsk->daddr = saddr; newsk->saddr = daddr; // 放到tcp的sock哈希表 put_sock(newsk->num,newsk); // tcp头 newsk->dummy_th.res1 = 0; newsk->dummy_th.doff = 6; newsk->dummy_th.fin = 0; newsk->dummy_th.syn = 0; newsk->dummy_th.rst = 0; newsk->dummy_th.psh = 0; newsk->dummy_th.ack = 0; newsk->dummy_th.urg = 0; newsk->dummy_th.res2 = 0; newsk->acked_seq = skb->h.th->seq + 1; newsk->copied_seq = skb->h.th->seq + 1; newsk->socket = NULL; newsk->ip_ttl=sk->ip_ttl; newsk->ip_tos=skb->ip_hdr->tos; rt=ip_rt_route(saddr, NULL,NULL); if(rt!=NULL && (rt->rt_flags&RTF_WINDOW)) newsk->window_clamp = rt->rt_window; else newsk->window_clamp = 0; if (sk->user_mss) newsk->mtu = sk->user_mss; else if(rt!=NULL && (rt->rt_flags&RTF_MSS)) newsk->mtu = rt->rt_mss - HEADER_SIZE; else { #ifdef CONFIG_INET_SNARL /* Sub Nets Are Local */ if ((saddr ^ daddr) & default_mask(saddr)) #else if ((saddr ^ daddr) & dev->pa_mask) #endif newsk->mtu = 576 - HEADER_SIZE; else newsk->mtu = MAX_WINDOW; } // mtu等于设备的mtu减去ip头和tcp头的大小 newsk->mtu = min(newsk->mtu, dev->mtu - HEADER_SIZE); // 解析tcp选项 tcp_options(newsk,skb->h.th); // 分配一个skb buff = newsk->prot->wmalloc(newsk, MAX_SYN_SIZE, 1, GFP_ATOMIC); // skb和sock关联,4个字节是用于tcp mss选项,告诉对端自己的mss buff->len = sizeof(struct tcphdr)+4; buff->sk = newsk; buff->localroute = newsk->localroute; t1 =(struct tcphdr *) buff->data; // 构造ip和mac头 tmp = sk->prot->build_header(buff, newsk->saddr, newsk->daddr, &ndev, IPPROTO_TCP, NULL, MAX_SYN_SIZE,sk->ip_tos,sk->ip_ttl); buff->len += tmp; // tcp头 t1 =(struct tcphdr *)((char *)t1 +tmp); memcpy(t1, skb->h.th, sizeof(*t1)); buff->h.seq = newsk->write_seq; t1->dest = skb->h.th->source; t1->source = newsk->dummy_th.source; t1->seq = ntohl(newsk->write_seq++); // 是个ack包,即第二次握手 t1->ack = 1; newsk->window = tcp_select_window(newsk); newsk->sent_seq = newsk->write_seq; t1->window = ntohs(newsk->window); t1->res1 = 0; t1->res2 = 0; t1->rst = 0; t1->urg = 0; t1->psh = 0; t1->syn = 1; t1->ack_seq = ntohl(skb->h.th->seq+1); t1->doff = sizeof(*t1)/4+1; ptr =(unsigned char *)(t1+1); ptr[0] = 2; ptr[1] = 4; ptr[2] = ((newsk->mtu) >> 8) & 0xff; ptr[3] =(newsk->mtu) & 0xff; tcp_send_check(t1, daddr, saddr, sizeof(*t1)+4, newsk); // 发送ack,即第二次握手 newsk->prot->queue_xmit(newsk, ndev, buff, 0); reset_xmit_timer(newsk, TIME_WRITE , TCP_TIMEOUT_INIT); // skb关联的socket为newsk,accept的时候摘取skb时即拿到该socket返回给应用层 skb->sk = newsk; // 把skb中数据的大小算在newsk中 sk->rmem_alloc -= skb->mem_len; newsk->rmem_alloc += skb->mem_len; // 插入监听型socket的接收队列,accept的时候摘取 skb_queue_tail(&sk->receive_queue,skb); // 连接队列节点个数加1 sk->ack_backlog++; release_sock(newsk); tcp_statistics.TcpOutSegs++; }
这个函数主要是生成一个sock结构体,挂载到skb中,然后把skb插入队列中。最后发送ack完成第二次握手。
继续来看第三次握手,前面说过tcp_rcv是处理tcp数据包的。所以还是回到这个函数。
// daddr,saddr是ip头的字段,len为tcp头+数据长度 int tcp_rcv( struct sk_buff *skb, struct device *dev, struct options *opt, unsigned long daddr, unsigned short len, unsigned long saddr, int redo, struct inet_protocol * protocol ){ // 从tcp的socket哈希链表中找到对应的socket结构 sk = get_sock(&tcp_prot, th->dest, saddr, th->source, daddr); …… if(sk->state==TCP_SYN_RECV) { tcp_set_state(sk, TCP_ESTABLISHED); } }
这里的逻辑很简单,就是设置sock结构体的状态为建立。但是我们发现,get_sock的时候,拿到的不再是listen型的那个sock结构体了,而是tcp_conn_request中生成的那个。因为tcp_conn_request生成的sock里设置了源ip、端口、目的ip、端口。get_sock匹配的时候会全匹配到这个新的sock。
三次握手的过程中,第一次握手的时候,在监听型的sock结构体的接收队列里插入了一个sock节点。在第三次握手的时候,修改这个sock状态为已连接。我们看看accept函数是怎么摘取这个队列中的节点的。
// 返回一个完成的连接 static struct sk_buff *tcp_dequeue_established(struct sock *s) { struct sk_buff *skb; unsigned long flags; save_flags(flags); cli(); skb=tcp_find_established(s); if(skb!=NULL) skb_unlink(skb); /* Take it off the queue */ restore_flags(flags); return skb; } // 找出已经完成三次握手的socket static struct sk_buff *tcp_find_established(struct sock *s) { struct sk_buff *p=skb_peek(&s->receive_queue); if(p==NULL) return NULL; do { if(p->sk->state == TCP_ESTABLISHED || p->sk->state >= TCP_FIN_WAIT1) return p; p=p->next; } while(p!=(struct sk_buff *)&s->receive_queue); return NULL; }
就是找到状态为TCP_ESTABLISHED的节点返回。另外监听型socket和通信型socket他的接收队列意义是不一样的,前者是已完成连接或者正在建立连接的队列,后者是数据包队列。
5.7利用gdb进行调试
6.网络层
网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息- -源站点和目的站点地址的网络地址。其主要任务包括
(1)路由处理,即选择下一跳
(2)添加 IP header
(3)计算 IP header checksum,用于检测 IP 报文头部在传播过程中是否出错
(4)可能的话,进行 IP 分片
(5)处理完毕,获取下一跳的 MAC 地址,设置链路层报文头,然后转入链路层处理。
IP协议栈的大致流程图:
6.1网络层发送数据过程简介
1.首先,ip_queue_xmit(skb)会检查skb->dst路由信息。如果没有,比如套接字的第一个包,就使用ip_route_output()选择一个路由。
2.接着,填充IP包的各个字段,比如版本、包头长度、TOS等。
3.中间的一些分片等,可参阅相关文档。基本思想是,当报文的长度大于mtu,gso的长度不为0就会调用 ip_fragment 进行分片,否则就会调用ip_finish_output2把数据发送出去。ip_fragment 函数中,会检查 IP_DF 标志位,如果待分片IP数据包禁止分片,则调用 icmp_send()向发送方发送一个原因为需要分片而设置了不分片标志的目的不可达ICMP报文,并丢弃报文,即设置IP状态为分片失败,释放skb,返回消息过长错误码。
4.接下来就用 ip_finish_ouput2 设置链路层报文头了。如果,链路层报头缓存有(即hh不为空),那就拷贝到skb里。如果没,那么就调用neigh_resolve_output,使用 ARP 获取。
6.2网络层接收数据过程简介
1.IP层的入口函数在 ip_rcv 函数。该函数首先会做包括 package checksum 在内的各种检查,如果需要的话会做 IP defragment(将多个分片合并),然后 packet 调用已经注册的 Pre-routing netfilter hook ,完成后最终到达 ip_rcv_finish 函数。
2.ip_rcv_finish 函数会调用 ip_router_input 函数,进入路由处理环节。它首先会调用 ip_route_input 来更新路由,然后查找 route,决定该 package 将会被发到本机还是会被转发还是丢弃: (1)如果是发到本机的话,调用 ip_local_deliver 函数,可能会做 de-fragment(合并多个 IP packet),然后调用 ip_local_deliver 函数。该函数根据 package 的下一个处理层的 protocal number,调用下一层接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。对于 TCP 来说,函数 tcp_v4_rcv 函数会被调用,从而处理流程进入 TCP 栈。(2)如果需要转发 (forward),则进入转发流程。该流程需要处理 TTL,再调用 dst_input 函数。该函数会 <1>处理 Netfilter Hook<2>执行 IP fragmentation<3>调用 dev_queue_xmit,进入链路层处理流程。
6.3网络层相关代码分析
ip宏定义实现
//IP首部长度 #define IP_HEADER_LEN 20 //IP版本号位置 以太网首部2+6+6,与下面那个在用的时候上区别下 #define IP_HEADER_LEN_VER_P 0xe //IP版本号位置 以太网首部2+6+6 #define IP_P 0xe //IP 16位标志位置 #define IP_FLAGS_P 0x14 //IP 生存时间位置 #define IP_TTL_P 0x16 //IP协议类型位置,如ICMP,TCP,UDP 1个字节 #define IP_PROTO_P 0x17 //首部校验和 #define IP_CHECKSUM_P 0x18 // IP源地址位置 14+12 #define IP_SRC_P 0x1a // IP目标地址位置 14+12+4 #define IP_DST_P 0x1e //IP总长度 #define IP_TOTLEN_H_P 0x10 #define IP_TOTLEN_L_P 0x11 //协议类型 #define IP_PROTO_ICMP_V 0x01 #define IP_PROTO_TCP_V 0x06 #define IP_PROTO_UDP_V 0x11
在IP数据包前增加以太网header
// make a return eth header from a received eth packet void make_eth( unsigned char *buf) { unsigned char i = 0; //copy the destination mac from the source and fill my mac into src while(i < sizeof(mac_addr)) { buf[ETH_DST_MAC + i] = buf[ETH_SRC_MAC + i]; buf[ETH_SRC_MAC + i] = macaddr[i]; i++; } }
判定是否发给本机的函数,填充函数,以及校验和函数
//判定过程与eth_type_is_arp_and_my_ip类似 unsigned char eth_type_is_ip_and_my_ip( unsigned char *buf, unsigned int len) { unsigned char i = 0; //eth+ip+udp header is 42 if(len < MIN_FRAMELEN) { return( 0); } if(buf[ETH_TYPE_H_P] != ETHTYPE_IP_H_V || buf[ETH_TYPE_L_P] != ETHTYPE_IP_L_V) { return( 0); } if(buf[IP_HEADER_LEN_VER_P] != 0x45) { // must be IP V4 and 20 byte header return( 0); } while(i < sizeof(ipv4_addr)) { if(buf[IP_DST_P + i] != ipaddr[i]) { return( 0); } i++; } return( 1); } //下面那个ip填充函数调用它,主要是补充填充和校验和 void fill_ip_hdr_checksum( unsigned char *buf) { unsigned int ck; // clear the 2 byte checksum buf[IP_CHECKSUM_P] = 0; buf[IP_CHECKSUM_P + 1] = 0; buf[IP_FLAGS_P] = 0x40; // don't fragment buf[IP_FLAGS_P + 1] = 0; // fragement offset buf[IP_TTL_P] = 64; // ttl // calculate the checksum: //校验和计算,在下下面那个函数里面,输入参数的含义下面看就晓得了 ck = checksum(&buf[IP_P], IP_HEADER_LEN, 0); buf[IP_CHECKSUM_P] = ck >> 8; buf[IP_CHECKSUM_P + 1] = ck & 0xff; } // make a return ip header from a received ip packet //与以太网填充函数类似,填充ip地址 void make_ip( unsigned char *buf) { unsigned char i = 0; while(i < sizeof(ipv4_addr)) { buf[IP_DST_P + i] = buf[IP_SRC_P + i]; buf[IP_SRC_P + i] = ipaddr[i]; i++; } fill_ip_hdr_checksum(buf); }
校验和的具体实现
unsigned int checksum( unsigned char * buf, unsigned int len, unsigned char type) { // type 0=ip // 1=udp // 2=tcp unsigned long sum = 0; //if(type==0){ // // do not add anything //} if(type== 1) { sum+=IP_PROTO_UDP_V; // protocol udp // the length here is the length of udp (data+header len) // =length given to this function - (IP.scr+IP.dst length) sum+=len- 8; // = real tcp len } if(type== 2) { sum+=IP_PROTO_TCP_V; // the length here is the length of tcp (data+header len) // =length given to this function - (IP.scr+IP.dst length) sum+=len- 8; // = real tcp len } // build the sum of 16bit words while(len > 1) { sum += 0xFFFF & (*buf<<8|*(buf+ 1)); buf+= 2; len-= 2; } // if there is a byte left then add it (padded with zero) if (len) { sum += (0xFF & *buf)<< 8; } // now calculate the sum over the bytes in the sum // until the result is only 16bit long while (sum>> 16) { sum = (sum & 0xFFFF)+(sum >> 16); } // build 1's complement: return( ( unsigned int) sum ^ 0xFFFF); }
6.4利用gdb进行调试
6.5ARP协议
ARP协议是“Address Resolution Protocol”(地址解析协议)的缩写。在局域网中,网络中实际传输的是“帧”,帧里面是有目标主机的MAC地址的。在以太网中,一个主机要和另一个主机进行直接通信,必须要知道目标主机的MAC地址。但这个目标MAC地址是如何获得的呢?它就是通过地址解析协议获得的。所谓“地址解析”就是主机在发送帧前将目标IP地址转换成目标MAC地址的过程。ARP协议的基本功能就是通过目标设备的IP地址,查询目标设备的MAC地址,以保证通信的顺利进行。
ARP协议是围着一个数组链表的数据结构进行的,包括对节点的增删改查,一些回调函数的设置。相关数据结构:
struct arp_table { struct arp_table *next; //该条数据上一次使用的时间,用来判断该数据是否已经过期 unsigned int flags; unsigned long ip; unsigned long mask; unsigned char ha[MAX_ADDR_LEN]; unsigned char hlen; unsigned short htype; struct device *dev; //定时器 struct timer_list timer; //重试的次数 int retries; //因为还没有完成ip和mac映射而导致无法发送的数据包,完成映射会进行处理这些数据包 struct sk_buff_head skb; };
ARP协议流程图:
6.6ARP协议相关代码分析
// 维护ip和mac地址映射的数组链表 struct arp_table { struct arp_table *next; /* Linked entry list */ // 该条数据上一次使用的时间,用来判断该数据是否已经过期 unsigned long last_used; /* For expiry */ // 该条数据的一些设置,比如是否是永久性的,如果是则不会过期,也就是会一直存在该数组链表中 unsigned int flags; /* Control status */ unsigned long ip; /* ip address of entry */ unsigned long mask; /* netmask - used for generalised proxy arps (tridge) */ unsigned char ha[MAX_ADDR_LEN]; /* Hardware address */ unsigned char hlen; /* Length of hardware address */ unsigned short htype; /* Type of hardware in use */ struct device *dev; /* Device the entry is tied to */
// 定时器 struct timer_list timer; /* expire timer */ // 重试的次数 int retries; /* remaining retries */ // 因为还没有完成ip和mac映射而导致无法发送的数据包,完成映射会进行处理这些数据包 struct sk_buff_head skb; /* list of queued packets */ }; // 如果第一个查询的包发出去后,ARP_RES_TIME长的时间还没有收到回复,就重新发一个 #define ARP_RES_TIME (250*(HZ/10)) // 最多重发ARP_MAX_TRIES次查询包 #define ARP_MAX_TRIES 3 // arp_table数组链表中的数据最多存活的时长 #define ARP_TIMEOUT (600*HZ) // 隔ARP_CHECK_INTERVAL秒查一次arp_table数组链表中的数据,看是否有过期的数据 #define ARP_CHECK_INTERVAL (60 * HZ) enum proxy { PROXY_EXACT=0, PROXY_ANY, PROXY_NONE, }; static void arp_check_expire (unsigned long); static struct arp_table *arp_lookup(unsigned long paddr, enum proxy proxy); static struct timer_list arp_timer = { NULL, NULL, ARP_CHECK_INTERVAL, 0L, &arp_check_expire }; // 默认掩码是全1 #define DEF_ARP_NETMASK (~0) //arp_table数组的大小,不包括代理的的 #define ARP_TABLE_SIZE 16 // 整个arp_table数组的大小,包括代理的 #define FULL_ARP_TABLE_SIZE (ARP_TABLE_SIZE+1) // 初始化arp_table数组链表 struct arp_table *arp_tables[FULL_ARP_TABLE_SIZE] = { NULL, }; // arp_table中的数组是通过hash的方式存储到相应的位置的,这里是hash算法的实现 #define HASH(paddr) (htonl(paddr) & (ARP_TABLE_SIZE - 1)) // 代理的位置索引 #define PROXY_HASH ARP_TABLE_SIZE // 隔一段时间检查arp_table中的数组,看是否有需要删除的 static void arp_check_expire(unsigned long dummy) { int i; // 当前时间 unsigned long now = jiffies; unsigned long flags; save_flags(flags); cli(); for (i = 0; i < FULL_ARP_TABLE_SIZE; i++){ struct arp_table *entry; // 指向整个arp_table数组链表 struct arp_table **pentry = &arp_tables[i]; while ((entry = *pentry) != NULL) { // 如果上一次使用的时间离现在超过了ARP_TIMEOUT的大小,并且该数组没有设置永久存储标记,则删除该数据 if ((now - entry->last_used) > ARP_TIMEOUT && !(entry->flags & ATF_PERM)) { *pentry = entry->next; /* remove from list */ // 清除定时器 del_timer(&entry->timer); /* Paranoia */ // 释放该数据对应的结构体 kfree_s(entry, sizeof(struct arp_table)); } else pentry = &entry->next; /* go to next entry */ } } restore_flags(flags); // 删除旧的定时器,增加新的定时器(重置定时器),add_timer函数会自动加上当前时间jiffies,所以只需要设置时间间隔ARP_CHECK_INTERVAL就行 del_timer(&arp_timer); arp_timer.expires = ARP_CHECK_INTERVAL; add_timer(&arp_timer); } /* 释放某条arp缓存相关的内存,包括: 1.挂在arp缓存结构体的sk_buff队列 2.定时器 3.arp缓存项对应的结构体 */ static void arp_release_entry(struct arp_table *entry) { struct sk_buff *skb; unsigned long flags; save_flags(flags); cli(); // 释放该数据对应的sk_buff,也就是因为没有完成映射导致还没有发送出去的数据包 while ((skb = skb_dequeue(&entry->skb)) != NULL) { skb_device_lock(skb); restore_flags(flags); dev_kfree_skb(skb, FREE_WRITE); } restore_flags(flags); // 释放定时器 del_timer(&entry->timer); // 释放该数据本身的结构体 kfree_s(entry, sizeof(struct arp_table)); return; } // 硬件有问题时释放该硬件对应的arp数据 int arp_device_event(unsigned long event, void *ptr) { struct device *dev=ptr; int i; unsigned long flags; if(event!=NETDEV_DOWN) return NOTIFY_DONE; save_flags(flags); cli(); // 遍历arp缓存数组链表 for (i = 0; i < FULL_ARP_TABLE_SIZE; i++) { struct arp_table *entry; struct arp_table **pentry = &arp_tables[i]; while ((entry = *pentry) != NULL) { // 找到和该设备相关的arp缓存项 if(entry->dev==dev) { *pentry = entry->next; /* remove from list */ del_timer(&entry->timer); /* Paranoia */ kfree_s(entry, sizeof(struct arp_table)); } else pentry = &entry->next; /* go to next entry */ } } restore_flags(flags); return NOTIFY_DONE; } // 发送arp包 void arp_send(int type, int ptype, unsigned long dest_ip, struct device *dev, unsigned long src_ip, unsigned char *dest_hw, unsigned char *src_hw){ struct sk_buff *skb; struct arphdr *arp; unsigned char *arp_ptr; if(dev->flags&IFF_NOARP) return; // 申请一个sk_buff和设置包的内容 skb = alloc_skb(sizeof(struct arphdr)+ 2*(dev->addr_len+4) + dev->hard_header_len, GFP_ATOMIC); if (skb == NULL) { printk("ARP: no memory to send an arp packet\n"); return; } // len为有效数据的长度,此时为arp头(sizeof(struct arphdr))+两个ip和硬件地址(2*(dev->addr_len+4))+mac头长度(dev->hard_header_len) skb->len = sizeof(struct arphdr) + dev->hard_header_len + 2*(dev->addr_len+4); skb->arp = 1; skb->dev = dev; skb->free = 1; // 设置mac头部 dev->hard_header(skb->data,dev,ptype,dest_hw?dest_hw:dev->broadcast,src_hw?src_hw:NULL,skb->len,skb); arp = (struct arphdr *) (skb->data + dev->hard_header_len); arp->ar_hrd = htons(dev->type); // 设置arp的上层协议 #ifdef CONFIG_AX25 arp->ar_pro = (dev->type != ARPHRD_AX25)? htons(ETH_P_IP) : htons(AX25_P_IP); #else arp->ar_pro = htons(ETH_P_IP); #endif arp->ar_hln = dev->addr_len; arp->ar_pln = 4; arp->ar_op = htons(type); arp_ptr=(unsigned char *)(arp+1); memcpy(arp_ptr, src_hw, dev->addr_len); arp_ptr+=dev->addr_len; memcpy(arp_ptr, &src_ip,4); arp_ptr+=4; if (dest_hw != NULL) memcpy(arp_ptr, dest_hw, dev->addr_len); else memset(arp_ptr, 0, dev->addr_len); arp_ptr+=dev->addr_len; memcpy(arp_ptr, &dest_ip, 4); // 传给链路层进行处理 dev_queue_xmit(skb, dev, 0); } // arp解析请求超时重发 static void arp_expire_request (unsigned long arg) { struct arp_table *entry = (struct arp_table *) arg; struct arp_table **pentry; unsigned long hash; unsigned long flags; save_flags(flags); cli(); // 说明该数据已经完成了映射 if (entry->flags & ATF_COM) { restore_flags(flags); return; } // 如果重传次数还大于0, if (--entry->retries > 0) { unsigned long ip = entry->ip; struct device *dev = entry->dev; // 重试定时器,如果ARP_RES_TIME秒后还没有回复,可能需要继续发送 /* Set new timer. */ del_timer(&entry->timer); entry->timer.expires = ARP_RES_TIME; add_timer(&entry->timer); restore_flags(flags); // 发送arp包 arp_send(ARPOP_REQUEST, ETH_P_ARP, ip, dev, dev->pa_addr, NULL, dev->dev_addr); return; } // 找到该数据对应的ip在arp_table里的位置 hash = HASH(entry->ip); if (entry->flags & ATF_PUBL) pentry = &arp_tables[PROXY_HASH]; else pentry = &arp_tables[hash]; // 删除解析失败的arp数据 while (*pentry != NULL) { if (*pentry == entry) { *pentry = entry->next; /* delete from linked list */ del_timer(&entry->timer); restore_flags(flags); arp_release_entry(entry); return; } pentry = &(*pentry)->next; } restore_flags(flags); printk("Possible ARP queue corruption.\n"); } // 完成某条数据的arp解析后,需要处理该数据对应的sk_buff链表 static void arp_send_q(struct arp_table *entry, unsigned char *hw_dest) { struct sk_buff *skb; unsigned long flags; // 该数据没有完成解析,则不能发送对应的数据包 if(!(entry->flags&ATF_COM)) { printk("arp_send_q: incomplete entry for %s\n", in_ntoa(entry->ip)); return; } save_flags(flags); cli(); // 处理该条数据对应的sk_buff链表 while((skb = skb_dequeue(&entry->skb)) != NULL) { IS_SKB(skb); skb_device_lock(skb); restore_flags(flags); // 系统发送数据包时,如果没有找到需要的ip和马刺地址的映射,则不会创建mac头,这里需要加上 if(!skb->dev->rebuild_header(skb->data,skb->dev,skb->raddr,skb)) { // 标记已经完成arp解析 skb->arp = 1; // 传给链路层处理 if(skb->sk==NULL) dev_queue_xmit(skb, skb->dev, 0); else dev_queue_xmit(skb,skb->dev,skb->sk->priority); } else { printk("arp_send_q: The impossible occurred. Please notify Alan.\n"); printk("arp_send_q: active entity %s\n",in_ntoa(entry->ip)); printk("arp_send_q: failed to find %s\n",in_ntoa(skb->raddr)); } } restore_flags(flags); } // 删除某个ip对应的arp缓存 void arp_destroy(unsigned long ip_addr, int force) { int checked_proxies = 0; struct arp_table *entry; struct arp_table **pentry; // 找到该ip对应的位置索引 unsigned long hash = HASH(ip_addr); ugly: cli(); pentry = &arp_tables[hash]; // 如果没找到,继续找arp代理的 if (! *pentry) /* also check proxy entries */ pentry = &arp_tables[PROXY_HASH]; while ((entry = *pentry) != NULL) { // 遍历链表,找到和该ip相等的数据 if (entry->ip == ip_addr) { // 判断是否是持久性的数据,如果是则根据force来判断是否强制删除 if ((entry->flags & ATF_PERM) && !force) return; *pentry = entry->next; del_timer(&entry->timer); sti(); arp_release_entry(entry); goto ugly; } pentry = &entry->next; // 检查完一般的arp缓存后,还需要检查arp代理链表 if (!checked_proxies && ! *pentry) { checked_proxies = 1; pentry = &arp_tables[PROXY_HASH]; } } sti(); } // 处理从链路层上报的数据包 int arp_rcv(struct sk_buff *skb, struct device *dev, struct packet_type *pt) { struct arphdr *arp = (struct arphdr *)skb->h.raw; unsigned char *arp_ptr= (unsigned char *)(arp+1); struct arp_table *entry; struct arp_table *proxy_entry; int addr_hint,hlen,htype; unsigned long hash; unsigned char ha[MAX_ADDR_LEN]; /* So we can enable ints again. */ long sip,tip; unsigned char *sha,*tha; // 硬件地址长度和类型是否相等,协议长度是否等于4,即ip协议,目前只支持这种 if (arp->ar_hln != dev->addr_len || dev->type != ntohs(arp->ar_hrd) || dev->flags & IFF_NOARP || arp->ar_pln != 4) { kfree_skb(skb, FREE_READ); return 0; } switch(dev->type) { #ifdef CONFIG_AX25 case ARPHRD_AX25: if(arp->ar_pro != htons(AX25_P_IP)) { kfree_skb(skb, FREE_READ); return 0; } break; #endif case ARPHRD_ETHER: case ARPHRD_ARCNET: if(arp->ar_pro != htons(ETH_P_IP)) { kfree_skb(skb, FREE_READ); return 0; } break; default: printk("ARP: dev->type mangled!\n"); kfree_skb(skb, FREE_READ); return 0; } // 硬件长度和类型 hlen = dev->addr_len; htype = dev->type; // arp层数据 // arp_ptr指向数据首地址,sha等于发送者的硬件地址 sha=arp_ptr; // 移动硬件长度hlen个字节 arp_ptr+=hlen; // sip等于发送者ip memcpy(&sip,arp_ptr,4); // 继续移动4个字节,即移动ip长度个字节 arp_ptr+=4; // 接收者的硬件地址 tha=arp_ptr; // 继续移动 arp_ptr+=hlen; // 接收者ip memcpy(&tip,arp_ptr,4); // 该包是自己发的 if(tip == INADDR_LOOPBACK) { kfree_skb(skb, FREE_READ); return 0; } // 检测接收者ip类型 addr_hint = ip_chk_addr(tip); // 如果该包是一个回复包 if(arp->ar_op == htons(ARPOP_REPLY)) { // 但是接收地址不是本机地址,则丢弃 if(addr_hint!=IS_MYADDR) { kfree_skb(skb, FREE_READ); return 0; } } // 是一个请求包 else { // 接收者地址不是本机,这时候需要判断本机代理的arp缓存中是否有接收者的数据 if(tip!=dev->pa_addr) { cli(); for(proxy_entry=arp_tables[PROXY_HASH]; proxy_entry; proxy_entry = proxy_entry->next) { if (proxy_entry->dev != dev && proxy_entry->htype == htype && !((proxy_entry->ip^tip)&proxy_entry->mask)) break; } // 找到了发送回复包 if (proxy_entry) { memcpy(ha, proxy_entry->ha, hlen); sti(); arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,ha); kfree_skb(skb, FREE_READ); return 0; } // 找不到则丢弃包 else { sti(); kfree_skb(skb, FREE_READ); return 0; } } // 接收者是本机的包,则发送回复包 else { arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr); } } // 更新arp缓存的数据 hash = HASH(sip); cli(); for(entry=arp_tables[hash];entry;entry=entry->next) if(entry->ip==sip && entry->htype==htype) break; // 如果本来就有发送者ip的缓存项,则更新数据 if(entry) { memcpy(entry->ha, sha, hlen); entry->hlen = hlen; entry->last_used = jiffies; // 判断该ip对应的arp项是否处于已经完成解析状态,不是的话,把他置为解析完成并且发送缓存在sk_buff中的包 if (!(entry->flags & ATF_COM)) { // 删除定时器 del_timer(&entry->timer); // 修改该数据的状态为解析完成 entry->flags |= ATF_COM; sti(); // 发送滞留的包 arp_send_q(entry, sha); } else { sti(); } } // 没有找到发送者ip对应的数据,则插入一条新的arp缓存项 else { entry = (struct arp_table *)kmalloc(sizeof(struct arp_table),GFP_ATOMIC); if(entry == NULL) { sti(); printk("ARP: no memory for new arp entry\n"); kfree_skb(skb, FREE_READ); return 0; } entry->mask = DEF_ARP_NETMASK; entry->ip = sip; entry->hlen = hlen; entry->htype = htype; entry->flags = ATF_COM; init_timer(&entry->timer); memcpy(entry->ha, sha, hlen); entry->last_used = jiffies; entry->dev = skb->dev; skb_queue_head_init(&entry->skb); // 头插法 entry->next = arp_tables[hash]; arp_tables[hash] = entry; sti(); } kfree_skb(skb, FREE_READ); return 0; } // 在根据ip在arp缓存表里找相关的数据,找不到则发送arp请求去找 int arp_find(unsigned char *haddr, unsigned long paddr, struct device *dev, unsigned long saddr, struct sk_buff *skb) { struct arp_table *entry; unsigned long hash; #ifdef CONFIG_IP_MULTICAST unsigned long taddr; #endif switch (ip_chk_addr(paddr)) { // 如果找的是本机ip和mac地址的映射,则返回本机的硬件地址,并设置完成解析标记 case IS_MYADDR: printk("ARP: arp called for own IP address\n"); memcpy(haddr, dev->dev_addr, dev->addr_len); skb->arp = 1; return 0; #ifdef CONFIG_IP_MULTICAST // 如果多播地址,则组装对应的mac地址 case IS_MULTICAST: if(dev->type==ARPHRD_ETHER || dev->type==ARPHRD_IEEE802) { // ip多播地址和mac多播地址的关系是ip多播地址的后23位直接映射到mac多播地址,而mac多播地址的前25位是固定的 // 前面25位是固定的 haddr[0]=0x01; haddr[1]=0x00; haddr[2]=0x5e; taddr=ntohl(paddr); // 取后8位进行赋值 haddr[5]=taddr&0xff; // 剩下的值往右挪8位 taddr=taddr>>8; // 再取8位进行赋值 haddr[4]=taddr&0xff; // 剩下的值往右挪8位 taddr=taddr>>8; // 取7位进行赋值即可 haddr[3]=taddr&0x7f; return 0; } #endif // 广播地址,则取mac广播地址进行赋值 case IS_BROADCAST: memcpy(haddr, dev->broadcast, dev->addr_len); skb->arp = 1; return 0; } hash = HASH(paddr); cli(); // 通过ip找对应的arp缓存,并设置不从代理里找 entry = arp_lookup(paddr, PROXY_NONE); // 找到 if (entry != NULL) /* It exists */ { // 缓存中有对应的数据但没有完成解析,先把数据包挂到该条数据的sk_buff队列中 if (!(entry->flags & ATF_COM)) { if (skb != NULL) { skb_queue_tail(&entry->skb, skb); skb_device_unlock(skb); } sti(); return 1; } // 缓存中有对应的数据并且已经解析完成 // 更新缓存中该条数据的信息 entry->last_used = jiffies; // 把找到的arp缓存项的mac地址赋值给haddr memcpy(haddr, entry->ha, dev->addr_len); // 设置arp解析完成标记位 if (skb) skb->arp = 1; sti(); return 0; } // 没有找到该ip对应的数据,则新增一条数据 entry = (struct arp_table *) kmalloc(sizeof(struct arp_table), GFP_ATOMIC); if (entry != NULL) { // 初始化新增数据的内容 entry->mask = DEF_ARP_NETMASK; entry->ip = paddr; entry->hlen = dev->addr_len; entry->htype = dev->type; entry->flags = 0; memset(entry->ha, 0, dev->addr_len); entry->dev = dev; entry->last_used = jiffies; init_timer(&entry->timer); entry->timer.function = arp_expire_request; entry->timer.data = (unsigned long)entry; entry->timer.expires = ARP_RES_TIME; entry->next = arp_tables[hash]; arp_tables[hash] = entry; add_timer(&entry->timer); entry->retries = ARP_MAX_TRIES; skb_queue_head_init(&entry->skb); if (skb != NULL) { skb_queue_tail(&entry->skb, skb); skb_device_unlock(skb); } } else { if (skb != NULL && skb->free) kfree_skb(skb, FREE_WRITE); } sti(); // 加完新增的数据后,发送arp进行ip和mac地址的解析 arp_send(ARPOP_REQUEST, ETH_P_ARP, paddr, dev, saddr, NULL, dev->dev_addr); return 1; } #define HBUFFERLEN 30 int arp_get_info(char *buffer, char **start, off_t offset, int length) { int len=0; off_t begin=0; off_t pos=0; int size; struct arp_table *entry; char hbuffer[HBUFFERLEN]; int i,j,k; const char hexbuf[] = "0123456789ABCDEF"; size = sprintf(buffer,"IP address HW type Flags HW address Mask\n"); pos+=size; len+=size; cli(); for(i=0; i<FULL_ARP_TABLE_SIZE; i++) { for(entry=arp_tables[i]; entry!=NULL; entry=entry->next) { #ifdef CONFIG_AX25 if(entry->htype==ARPHRD_AX25) strcpy(hbuffer,ax2asc((ax25_address *)entry->ha)); else { #endif for(k=0,j=0;k<HBUFFERLEN-3 && j<entry->hlen;j++) { hbuffer[k++]=hexbuf[ (entry->ha[j]>>4)&15 ]; hbuffer[k++]=hexbuf[ entry->ha[j]&15 ]; hbuffer[k++]=':'; } hbuffer[--k]=0; #ifdef CONFIG_AX25 } #endif size = sprintf(buffer+len, "%-17s0x%-10x0x%-10x%s", in_ntoa(entry->ip), (unsigned int)entry->htype, entry->flags, hbuffer); size += sprintf(buffer+len+size, " %-17s\n", entry->mask==DEF_ARP_NETMASK? "*":in_ntoa(entry->mask)); len+=size; pos=begin+len; if(pos<offset) { len=0; begin=pos; } if(pos>offset+length) break; } } sti(); *start=buffer+(offset-begin); /* Start of wanted data */ len-=(offset-begin); /* Start slop */ if(len>length) len=length; /* Ending slop */ return len; } // 根据ip到arp缓存里找arp项,找不到就返回空 static struct arp_table *arp_lookup(unsigned long paddr, enum proxy proxy) { struct arp_table *entry; unsigned long hash = HASH(paddr); for (entry = arp_tables[hash]; entry != NULL; entry = entry->next) if (entry->ip == paddr) break; /* it's possibly a proxy entry (with a netmask) */ // 如果找不到并且没有设置不需要从代理里找,则到代理中找 if (!entry && proxy != PROXY_NONE){ for (entry=arp_tables[PROXY_HASH]; entry != NULL; entry = entry->next){ /* 代理匹配两种方式,一种是精确匹配(PROXY_EXACT),一直是网络号匹配即可(PROXY_ANY) (entry->ip^paddr)&entry->mask)逻辑为,异或是不相等的话结果是1,否则为0,比如0^1等于1, 所以entry->ip^paddr比较好,前n位如果相等则结果的前n位都是0,而entry->mask是前n位是网络号且全 为1,后面为全0,如果(entry->ip^paddr)&entry->mask)结果为0,所说明entry->ip^paddr的结果中,前n位 等于0的个数大于等于mask中前面的1,说明网络号是一样的。 */ if ((proxy==PROXY_EXACT) ? (entry->ip==paddr) : !((entry->ip^paddr)&entry->mask)) break; } } return entry; } // 修改或新增arp缓存项 static int arp_req_set(struct arpreq *req) { struct arpreq r; struct arp_table *entry; struct sockaddr_in *si; int htype, hlen; unsigned long ip; struct rtable *rt; memcpy_fromfs(&r, req, sizeof(r)); /* We only understand about IP addresses... */ if (r.arp_pa.sa_family != AF_INET) return -EPFNOSUPPORT; switch (r.arp_ha.sa_family) { case ARPHRD_ETHER: htype = ARPHRD_ETHER; hlen = ETH_ALEN; break; case ARPHRD_ARCNET: htype = ARPHRD_ARCNET; hlen = 1; /* length of arcnet addresses */ break; #ifdef CONFIG_AX25 case ARPHRD_AX25: htype = ARPHRD_AX25; hlen = 7; break; #endif default: return -EPFNOSUPPORT; } si = (struct sockaddr_in *) &r.arp_pa; ip = si->sin_addr.s_addr; if (ip == 0) { printk("ARP: SETARP: requested PA is 0.0.0.0 !\n"); return -EINVAL; } // ip是否可达,不可达的ip不允许更新 rt = ip_rt_route(ip, NULL, NULL); if (rt == NULL) return -ENETUNREACH; cli(); // 如果已经存在该ip对应的arp缓存项,使用精确匹配 entry = arp_lookup(ip, PROXY_EXACT); // 新的缓存项和原来的标记位不一样,则先删除再新增 if (entry && (entry->flags & ATF_PUBL) != (r.arp_flags & ATF_PUBL)) { sti(); arp_destroy(ip,1); cli(); entry = NULL; } // entry为NULL可能是找不到该ip对应的缓存项,或者找到了,但是标记位不一样,被删除了,这里需要新增 if (entry == NULL) { // 先预先得到一个位置索引 unsigned long hash = HASH(ip); // 如果设置了ATF_PUBL标记位,说明该arp项应该挂到arp代理的链表中 if (r.arp_flags & ATF_PUBL) hash = PROXY_HASH; entry = (struct arp_table *) kmalloc(sizeof(struct arp_table), GFP_ATOMIC); if (entry == NULL) { sti(); return -ENOMEM; } entry->ip = ip; entry->hlen = hlen; entry->htype = htype; init_timer(&entry->timer); // 头插法 entry->next = arp_tables[hash]; arp_tables[hash] = entry; skb_queue_head_init(&entry->skb); } // 此处的entry代表的可能是一个新生成的arp项,也可能是arp缓存链表里原本就存在的项 memcpy(&entry->ha, &r.arp_ha.sa_data, hlen); entry->last_used = jiffies; /* 在原有的标记位上,追加设置该arp缓存项已经解析完成标记位 或者逻辑为: 如果arp_flags是0,证明没有任何标记位,与运算后arp_flags等于ATF_COM的值 如果arp_flags是等于ATF_COM的值,证明之前已经设置了该标记位,与运算后,值不变 如果arp_flags为其他的标记位,则标记位累加。具体可见下面的标记位,他们都有自己的位,不会冲突。 #define ATF_COM 0x02 #define ATF_PERM 0x04 #define ATF_PUBL 0x08 #define ATF_USETRAILERS 0x10 #define ATF_NETMASK 0x20 */ entry->flags = r.arp_flags | ATF_COM; // 设置了这个两个位说明是一个代理项,则对掩码进行赋值,否则使用DEF_ARP_NETMASK作为掩码 if ((entry->flags & ATF_PUBL) && (entry->flags & ATF_NETMASK)) { si = (struct sockaddr_in *) &r.arp_netmask; entry->mask = si->sin_addr.s_addr; } else entry->mask = DEF_ARP_NETMASK; entry->dev = rt->rt_dev; sti(); return 0; } // 获取arp缓存项 static int arp_req_get(struct arpreq *req) { struct arpreq r; struct arp_table *entry; struct sockaddr_in *si; memcpy_fromfs(&r, req, sizeof(r)); // 只支持AF_INET协议簇 if (r.arp_pa.sa_family != AF_INET) return -EPFNOSUPPORT; si = (struct sockaddr_in *) &r.arp_pa; cli(); // 通过ip找arp缓存项 entry = arp_lookup(si->sin_addr.s_addr,PROXY_ANY); if (entry == NULL) { sti(); return -ENXIO; } // 赋值硬件信息 memcpy(r.arp_ha.sa_data, &entry->ha, entry->hlen); r.arp_ha.sa_family = entry->htype; // arp缓存项标记位赋值 r.arp_flags = entry->flags; sti(); memcpy_tofs(req, &r, sizeof(r)); return 0; } // 操作arp_table里的数据,从而管理arp缓存 int arp_ioctl(unsigned int cmd, void *arg) { struct arpreq r; struct sockaddr_in *si; int err; switch(cmd) { case SIOCDARP: // 权限校验 if (!suser()) return -EPERM; err = verify_area(VERIFY_READ, arg, sizeof(struct arpreq)); if(err) return err; memcpy_fromfs(&r, arg, sizeof(r)); if (r.arp_pa.sa_family != AF_INET) return -EPFNOSUPPORT; si = (struct sockaddr_in *) &r.arp_pa; // 删除 arp_destroy(si->sin_addr.s_addr, 1); return 0; case SIOCGARP: err = verify_area(VERIFY_WRITE, arg, sizeof(struct arpreq)); if(err) return err; // 查询 return arp_req_get((struct arpreq *)arg); case SIOCSARP: if (!suser()) return -EPERM; err = verify_area(VERIFY_READ, arg, sizeof(struct arpreq)); if(err) return err; // 修改、增加 return arp_req_set((struct arpreq *)arg); default: return -EINVAL; } /*NOTREACHED*/ return 0; } // 挂到链路层packet_type链表的节点,链路层收到arp包会调用arp_rcv函数进行处理 static struct packet_type arp_packet_type = { 0, /* Should be: __constant_htons(ETH_P_ARP) - but this _doesn't_ come out constant! */ NULL, /* All devices */ arp_rcv, NULL, NULL }; // 硬件设备状态变更会调用arp_device_event处理 static struct notifier_block arp_dev_notifier={ arp_device_event, NULL, 0 }; // 初始化arp协议 void arp_init (void) { /* Register the packet type */ // 注册arp协议到链路层,链路层收到包后会根据packet_type链表判断上层协议,然后上报数据包 arp_packet_type.type=htons(ETH_P_ARP); dev_add_pack(&arp_packet_type); /* Start with the regular checks for expired arp entries. */ // 系统启动时就开始隔段时间检查arp缓存的数据 add_timer(&arp_timer); /* Register for device down reports */ // 注册回调事件,arp的数据和具体硬件设备有关,所以设备状态发生变化时,需要通知arp协议进行处理 register_netdevice_notifier(&arp_dev_notifier); }
7.数据链路层和物理层
功能上,在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路,通过差错控制提供数据帧(Frame)在信道上无差错的传输,并进行各电路上的动作系列。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。在这一层,数据的单位称为帧(frame)。数据链路层协议的代表包括:SDLC、HDLC、PPP、STP、帧中继等。
实现上,Linux 提供了一个 Network device 的抽象层,其实现在 linux/net/core/dev.c。具体的物理网络设备在设备驱动中(driver.c)需要实现其中的虚函数。Network Device 抽象层调用具体网络设备的函数。
7.1物理层接收数据过程简介
1.物理层在收到发送请求之后,通过 DMA 将该主存中的数据拷贝至内部RAM(buffer)之中。在数据拷贝中,同时加入符合以太网协议的相关header,IFG、前导符和CRC。对于以太网网络,物理层发送采用CSMA/CD,即在发送过程中侦听链路冲突。
2.一旦网卡完成报文发送,将产生中断通知CPU,然后驱动层中的中断处理程序就可以删除保存的 skb 了。
7.2发送数据过程简介
1.一个package 到达机器的物理网络适配器,当它接收到数据帧时,就会触发一个中断,并将通过 DMA 传送到位于 linux kernel 内存中的 rx_ring。
2.网卡发出中断,通知 CPU 有个 package 需要它处理。中断处理程序主要进行以下一些操作,包括分配 skb_buff 数据结构,并将接收到的数据帧从网络适配器I/O端口拷贝到skb_buff 缓冲区中;从数据帧中提取出一些信息,并设置 skb_buff 相应的参数,这些参数将被上层的网络协议使用,例如skb->protocol;
3.终端处理程序经过简单处理后,发出一个软中断(NET_RX_SOFTIRQ),通知内核接收到新的数据帧。
4.内核中引入一组新的 API 来处理接收的数据帧,即 NAPI。所以,驱动有两种方式通知内核:(1) 通过以前的函数netif_rx;(2)通过NAPI机制。该中断处理程序调用 Network device的 netif_rx_schedule 函数,进入软中断处理流程,再调用 net_rx_action 函数。
5.该函数关闭中断,获取每个 Network device 的 rx_ring 中的所有 package,最终 pacakage 从 rx_ring 中被删除,进入 netif _receive_skb 处理流程。
6.netif_receive_skb 是链路层接收数据报的最后一站。它根据注册在全局数组 ptype_all 和 ptype_base 里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。该函数主要就是调用第三层协议的接收函数处理该skb包,进入第三层网络层处理。
7.3利用gdb进行调试
8.Linux系统中的中断分析
在计算机的发展过程中,外部设备的速度长期低于CPU,为了在必要的时候获取CPU的注意,需要在外设和CPU之间提供一种中断的机制。同时,对于现代操作系统,其内部的运行依赖于时钟的驱动,这也需要中断的支持。
不管是外部中断还是内部中断,在它们到达CPU之前,都会首先经过一个叫做中断控制器(Interrupt Controller)的硬件,其作用是根据中断源(Interrupt Source,也叫IRQ - Interrupt Request)的优先级对中断进行派发。
Linux中描述中断控制器的数据结构是struct irq_chip,因为不同芯片的中断控制器对其挂接的IRQ有不同的控制方法,因而这个结构体主要是由一组用于回调(callback),指向系统实际的中断控制器所使用的控制方法的函数指针构成。
struct irq_chip { const char *name; void (*irq_enable)(struct irq_data *data); void (*irq_disable)(struct irq_data *data); void (*irq_mask)(struct irq_data *data); void (*irq_unmask)(struct irq_data *data); void (*ipi_send_single)(struct irq_data *data, unsigned int cpu); void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest); ... };
IRQ是由struct irq_desc表示的:
struct irq_desc { const char *name; unsigned int depth; struct irqaction *action; struct irq_data irq_data; struct cpumask *percpu_enabled; ... };
"irq_desc"中的"action"是IRQ对应的中断处理函数(ISR - Interrupt Service Routine),按理ISR应该就是一个函数指针,可这里确是一个指向struct irqaction的指针。
struct irqaction { irq_handler_t handler; void *dev_id; struct irqaction *next; ... }
"irq_desc"中的"irq_data",这个在前面已经出现过了,"irq_chip"结构体中的每个函数指针,都会携带一个指向struct irq_data的指针作为参数,可见这个"irq_data"与中断控制必定关系密切。也就是说,在"irq_desc"的定义中,与中断控制器紧密联系的这部分被单独提取出来,构成了"irq_data"结构体:
struct irq_data { struct irq_chip *chip; struct irq_domain *domain; unsigned int irq; unsigned long hwirq; ... };
其中,"chip"就指向了这个IRQ所挂接的中断控制器,两者的绑定是通过irq_set_chip()函数完成的。
int irq_set_chip(unsigned int irq, struct irq_chip *chip) { struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0); desc->irq_data.chip = chip; ... }
struct irq_domain就承担着将中断控制器local的物理中断号转换成Linux全局的虚拟中断号的任务
struct irq_domain { const char *name; const struct irq_domain_ops *ops; irq_hw_number_t hwirq_max; struct radix_tree_root revmap_tree; unsigned int revmap_size; unsigned int linear_revmap[]; unsigned int revmap_direct_max_irq; ... };
具体的映射过程是由irq_domain"中的"irq_domain_ops"域所定义的函数来完成的:
struct irq_domain_ops { int (*match)(struct irq_domain *d, struct device_node *node, enum irq_domain_bus_token bus_token); int (*xlate)(struct irq_domain *d, struct device_node *node, const u32 *intspec, unsigned int intsize, unsigned long *out_hwirq, unsigned int *out_type); int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw); void (*unmap)(struct irq_domain *d, unsigned int virq); ... }
当中断经过中断控制器到达CPU后,Linux会首先通过irq_find_mapping()函数,根据物理中断号"hwirq"的值,查找包含映射关系的radix tree或者线性数组,得到"hwirq"对应的虚拟中断号"irq"。
unsigned int irq_find_mapping(struct irq_domain *domain, irq_hw_number_t hwirq) { struct irq_data *data; //线性映射的查找 if (hwirq < domain->revmap_size) return domain->linear_revmap[hwirq]; //radix tree映射的查找 rcu_read_lock(); data = radix_tree_lookup(&domain->revmap_tree, hwirq); rcu_read_unlock(); return data ? data->irq : 0; }
进入ISR之前自然要先保存被中断的线程的上下文,这主要是由set_irq_regs()函数来完成的:
struct pt_regs *set_irq_regs(struct pt_regs *new_regs) { struct pt_regs *old_regs = get_irq_regs(); __this_cpu_write(irq_regs, new_regs); return old_regs; }
9.课程总结
课上老师对Linux内核部分代码进行详解,在已经学习TCP/IP协议栈的理论知识的基础上,对各协议的具体实现细节进行了了解。实验上,通过自己搭建调试Linux内核代码的环境,并且对相关协议的实现进行跟踪分析,因此,学完本课程,不仅巩固了原有的基本理论知识,更将知识落在代码上,进行详细的理解与分析,对以后网络方面项目的开发奠定了基础。