网络层实现初步探究(linux网络协议栈笔记)
2016-12-15 22:04
417 查看
从Ping 127.0.0.1开始
Ping是潜水艇人员的专用术语,表示回应的声纳脉冲,在网络中Ping 是一个十分好用的TCP/IP工具。它主要的功能是用来检测网络的连通情况和分析网络速度。先给出一段部分ping的代码基本实现:
/****统计数据函数****/ void statistics(int sig) { computer_rtt(); //计算rtt printf("\n------ %s ping statistics ------\n",addr[0]); printf("%d packets transmitted,%d received,%d%% packet loss,time %.f ms\n", nsend,nreceived,(nsend-nreceived)/nsend*100,all_time); printf("rtt min/avg/max/mdev = %.3f/%.3f/%.3f/%.3f ms\n", min,avg,max,mdev); close(sockfd); exit(1); } /****检验和算法****/ unsigned short cal_chksum(unsigned short *addr,int len) { int nleft = len; int sum = 0; unsigned short *w = addr; unsigned short check_sum = 0; while(nleft>1) //ICMP包头以字(2字节)为单位累加 { sum += *w++; nleft -= 2; } if(nleft == 1) //ICMP为奇数字节时,转换最后一个字节,继续累加 { *(unsigned char *)(&check_sum) = *(unsigned char *)w; sum += check_sum; } sum = (sum >> 16) + (sum & 0xFFFF); sum += (sum >> 16); check_sum = ~sum; //取反得到校验和 return check_sum; } /*设置ICMP报头*/ int pack(int pack_no) { int i,packsize; struct icmp *icmp; struct timeval *tval; icmp = (struct icmp*)sendpacket; icmp->icmp_type = ICMP_ECHO; //ICMP_ECHO类型的类型号为0 icmp->icmp_code = 0; icmp->icmp_cksum = 0; icmp->icmp_seq = pack_no; //发送的数据报编号 icmp->icmp_id = pid; packsize = 8 + datalen; //数据报大小为64字节 tval = (struct timeval *)icmp->icmp_data; gettimeofday(tval,NULL); //记录发送时间 //校验算法 icmp->icmp_cksum = cal_chksum((unsigned short *)icmp,packsize); return packsize; } /****发送三个ICMP报文****/ void send_packet() { int packetsize; if(nsend < MAX_NO_PACKETS) { nsend++; packetsize = pack(nsend); //设置ICMP报头 //发送数据报 if(sendto(sockfd,sendpacket,packetsize,0, (struct sockaddr *)&dest_addr,sizeof(dest_addr)) < 0) { perror("sendto error"); } } } /****接受所有ICMP报文****/ void recv_packet() { int n,fromlen; extern int error; fromlen = sizeof(from); if(nreceived < nsend) { //接收数据报 if((n = recvfrom(sockfd,recvpacket,sizeof(recvpacket),0, (struct sockaddr *)&from,&fromlen)) < 0) { perror("recvfrom error"); } gettimeofday(&tvrecv,NULL); //记录接收时间 unpack(recvpacket,n); //剥去ICMP报头 nreceived++; } } /******剥去ICMP报头******/ int unpack(char *buf,int len) { int i; int iphdrlen; //ip头长度 struct ip *ip; struct icmp *icmp; struct timeval *tvsend; double rtt; ip = (struct ip *)buf; iphdrlen = ip->ip_hl << 2; //求IP报文头长度,即IP报头长度乘4 icmp = (struct icmp *)(buf + iphdrlen); //越过IP头,指向ICMP报头 len -= iphdrlen; //ICMP报头及数据报的总长度 if(len < 8) //小于ICMP报头的长度则不合理 { printf("ICMP packet\'s length is less than 8\n"); return -1; } //确保所接收的是所发的ICMP的回应 if((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == pid)) { tvsend = (struct timeval *)icmp->icmp_data; tv_sub(&tvrecv,tvsend); //接收和发送的时间差 //以毫秒为单位计算rtt rtt = tvrecv.tv_sec*1000 + tvrecv.tv_usec/1000; temp_rtt[nreceived] = rtt; all_time += rtt; //总时间 //显示相关的信息 printf("%d bytes from %s: icmp_seq=%u ttl=%d time=%.1f ms\n", len,inet_ntoa(from.sin_addr), icmp->icmp_seq,ip->ip_ttl,rtt); } else return -1; } //两个timeval相减 void tv_sub(struct timeval *recvtime,struct timeval *sendtime) { long sec = recvtime->tv_sec - sendtime->tv_sec; long usec = recvtime->tv_usec - sendtime->tv_usec; if(usec >= 0){ recvtime->tv_sec = sec; recvtime->tv_usec = usec; }else{ recvtime->tv_sec = sec - 1; recvtime->tv_usec = -usec; } } /*主函数*/ main(int argc,char *argv[]) { struct hostent *host; struct protoent *protocol; unsigned long inaddr = 0; // int waittime = MAX_WAIT_TIME; int size = 50 * 1024; addr[0] = argv[1]; //参数小于两个 if(argc < 2) { printf("usage:%s hostname/IP address\n",argv[0]); exit(1); } //不是ICMP协议 if((protocol = getprotobyname("icmp")) == NULL) { perror("getprotobyname"); exit(1); } //生成使用ICMP的原始套接字,只有root才能生成 if((sockfd = socket(AF_INET,SOCK_RAW,protocol->p_proto)) < 0) { perror("socket error"); exit(1); } //回收root权限,设置当前权限 setuid(getuid()); /*扩大套接字的接收缓存区导50K,这样做是为了减小接收缓存区溢出的 可能性,若无意中ping一个广播地址或多播地址,将会引来大量的应答*/ setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size)); bzero(&dest_addr,sizeof(dest_addr)); //初始化 dest_addr.sin_family = AF_INET; //套接字域是AF_INET(网络套接字) //判断主机名是否是IP地址 if(inet_addr(argv[1]) == INADDR_NONE) { if((host = gethostbyname(argv[1])) == NULL) //是主机名 { perror("gethostbyname error"); exit(1); } memcpy((char *)&dest_addr.sin_addr,host->h_addr,host->h_length); } else{ //是IP 地址 dest_addr.sin_addr.s_addr = inet_addr(argv[1]); } pid = getpid(); printf("PING %s(%s):%d bytes of data.\n",argv[1], inet_ntoa(dest_addr.sin_addr),datalen); //当按下ctrl+c时发出中断信号,并开始执行统计函数 signal(SIGINT,statistics); while(nsend < MAX_NO_PACKETS){ sleep(1); //每隔一秒发送一个ICMP报文 send_packet(); //发送ICMP报文 recv_packet(); //接收ICMP报文 } return 0; }
从上面的代码可以看出,ping使用了
SOCK_RAW的方式调用socket系统接口。
SOCK_RAW的含义即基于IP层协议建立的通信机制。Ping回送地址是为了检查本地的TCP/IP协议有没有设置好;Ping本机IP地址,这样是为了检查本机的IP地址是否设置有误;Ping本网网关或本网IP地址,这样的是为了检查硬件设备是否有问题,也可以检查本机与本地网络连接是否正常(在非局域网中这一步骤可以忽略);Ping远程IP地址,这主要是检查本网或本机与外部的连接是否正常。如果连本地址无法Ping通,则表明本地机TCP/IP协议不能正常工作。
Socket系统调用
回想一下socket调用到inet_create的时候,当如果是RAW应用的话,就会有如下的代码片段,
static int inet_create(struct socket *sock, int protocol)if (SOCK_RAW == sock->type) { ...... /*这几句赋值很有意义,我们要在send的实现代码中经常看到inet->hdrincl这个变量,记住,在目前的ping应用中,这个值是0,因为我们应用层设置的protocol是IPPROTO_ICMP。还有,num也被赋予一个值,大家应该记住,num其实就是TCP/UDP中提到的端口号,不过在RAW IP中这个num表示一种协议而已,不是真正的“端口号”*/ inet->num = protocol; if (IPPROTO_RAW == protocol) inet->hdrincl = 1; ...... /* 这里sk_protocol是IPPROTO_ICMP*/ sk->sk_protocol = protocol; /*下面执行的是raw_prot中的raw_init函数*/ if (sk->sk_prot->init) { err = sk->sk_prot->init(sk); ...... }
再次碰到的socket函数参数发生了变化,在
inet_create函数中创建socket时
sock->ops指向了
inet_sockraw_ops,而
sk->sk_prot指向了
raw_prot。
IP数据报文格式
帧结构是非常重要的一部分,对于网络协议,报文帧结构就是核心,所有的代码都围绕这些结构来运行IP协议用于管理客户端和服务器端之间的报文传送。普通的I P首部长为2 0个字节,除非含有选项字段。
IP头结构如下:
分析上图中的首部。最高位在左边,记为0 bit;最低位在右边,记为31 bit。
4个字节的32 bit值以下面的次序传输:首先是0~7 bit,其次8~15 bit,然后1 6~23 bit, 最后是24~31 bit。
这种传输次序称作big endian字节序。由于T C P / I P首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。以其他形式存储二进制整数的机器,如little endian格式,则必须在传输数据之前把首部转换成网络字节序。目前的协议版本号是4,因此I P有时也称作IPv4。
首部长度指的是首部占32 bit字的数目,包括任何选项。由于它是一个4比特字段,因此首部最长为60个字节。普通IP数据报(没有任何选择项)字段的值是5。
服务类型(TOS)字段包括一个3 bit的优先权子字段(现在已被忽略),4 bit的TOS子字段和1 bit未用位但必须置0。4 bit的TOS分别代表:最小时延、最大吞吐量、最高可靠性和最小费用。4 bit中只能置其中1 bit。如果所有4 bit均为0,那么就意味着是一般服务。RFC 1340[Reynolds and Postel 1992] 描述了所有的标准应用如何设置这些服务类型。RFC 1349[Almquist 1992]对该RFC进行了修正,更为详细地描述了TOS的特性。
send系统调用
socket应用程序一般使用send或sendto系统调用发送数据,那么它们到底什么区别呢?下面的图会给你一些答案。(注意,过去的内核版本是单独把send作为一个系统调用放在系统调用表中,现在是统一经过sys_socketcall系统调用接口转进来。不过对于我们没有什么区别,同样的,之后要介绍的recv、bind、listen等函数都如此)
在用户态调用的系统接口send和sendto及sendmsg,其实都调用了
sock_sendmsg,而且用得较多的send调用的sendto,只是参数要注意,内核代码如下:
sys_sendto(fd, buff, len, flags, NULL, 0);即最后两个参数为0(NULL)。在BSD Socket层使用
msghdr{}结构保存数据,在INET Socket层以下都使用
sk_buff{ }结构保存数据。
msghdr{ }结构是出于对4.4BSD的兼容性而定义的,内核内部函数
sock_sendmsg(),
sock_recvmsg()都使用这个结构。其定义如下:
struct msghdr { void *msg_name;// 存数据包的目的地址,网络包指向sockaddr_in //向内核发数据时,指向sockaddr_nl int msg_namelen;// 地址长度 struct iovec *msg_iov;/* 发送或接收的数据块结构*/ __kernel_size_t msg_iovlen;/* 数据块的个数*/ void *msg_control;/* 每个协议一个magic数(eg BSD 传递文件描述符) */ __kernel_size_t msg_controllen;/* cmsg链表的长度*/ unsigned msg_flags; };
msg_name和
msg_namelen就是数据报文要发向的对端的地址信息(即sendto系统调用中的
addr和
addr_len),当使用send时,它们的值为NULL和0。
struct iovec { void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */ __kernel_size_t iov_len; /* Must be size_t (1003.1g) */ };
此结构表示存放待发送数据的一个缓冲区,
iov_base是缓冲区的起始地址,指向用户层待发送数据,
iov_len是缓冲区的长度。
msg_iovlen是缓冲区的数量,对于sendto和send来讲,
msg_iovlen都是1。
msg_flags即为传入的参数flags,现在暂时不过多的关注flags的应用。
msg_control和
msg_controllen暂时不关注。
参照上图,看到虽然TCP/UDP/RAWIP的ops分别有一个,这里的RAW IP对应的ops是
inet_sockraw_ops,其对应的sendmsg函数指针成员都指向
inet_sendmsg,而且其它两种协议的sendmsg函数指针也指向到这个函数。
int inet_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size) { struct sock *sk = sock->sk; sock_rps_record_flow(sk); /* We may need to bind the socket. */ /* 我们可能要绑定这个socket. 这里要提醒的是RAW IP也有自己的bind函数,只不过此bind非彼bind,由于在inet_create函数中针对RAW socket曾经做了inet->num = protocol这么一个动作,而在ping应用程序中protocol是IPPROTO_ICMP(1),所以num=1,于是下面这句话不会被执行。*/ if (!inet_sk(sk)->inet_num && !sk->sk_prot->no_autobind && inet_autobind(sk)) return -EAGAIN; return sk->sk_prot->sendmsg(iocb, sk, msg, size); }
什么情况下会执行
inet_autobind函数呢?就是当创建UDP/TCP的socket后即调用send函数发送数据(这是一般客户端代码的行为方式),于是就会调用到此函数,因为之前的
inet_create函数没有给这种类型的socket指定num。所以凡是TCP/UDP的客户端必定会进入到此函数中。那么
inet_autobind到底干嘛呢?它里面调用了
sk->sk_prot->get_port,很明显,由于RAW IP没有实现
get_port函数,所以只能是调用
udp/tcp_v4_get_port函数获得未使用的port号赋给
inet->num,然后又把它赋给
inet->sport成员。
由于是raw类型的socket,所以实际指向
raw_prot结构。那么相应的sendmsg函数指针就是
raw_sendmsg函数。
对于使用RAW选项打开的socket,其对应的函数是
raw_prot->raw_sendmsg,
static int raw_sendmsg(struct sock *sk, struct msghdr *msg, size_t len) { struct inet_sock *inet = inet_sk(sk); struct net *net = sock_net(sk); struct ipcm_cookie ipc; struct rtable *rt = NULL; struct flowi4 fl4; int free = 0; __be32 daddr; __be32 saddr; u8 tos; int err; struct ip_options_data opt_copy; struct raw_frag_vec rfv; err = -EMSGSIZE; if (len > 0xFFFF) goto out; /* * Check the flags. */ err = -EOPNOTSUPP; if (msg->msg_flags & MSG_OOB) /* Mirror BSD error message */ goto out; /* compatibility */ /* * Get and verify the address. */ if (msg->msg_namelen) { DECLARE_SOCKADDR(struct sockaddr_in *, usin, msg->msg_name); err = -EINVAL; if (msg->msg_namelen < sizeof(*usin)) goto out; if (usin->sin_family != AF_INET) { pr_info_once("%s: %s forgot to set AF_INET. Fix it!\n", __func__, current->comm); err = -EAFNOSUPPORT; if (usin->sin_family) goto out; } daddr = usin->sin_addr.s_addr; /* ANK: I did not forget to get protocol from port field. * I just do not know, who uses this weirdness. * IP_HDRINCL is much more convenient. */ } else { err = -EDESTADDRREQ; if (sk->sk_state != TCP_ESTABLISHED) goto out; daddr = inet->inet_daddr; } ipc.sockc.tsflags = sk->sk_tsflags; ipc.addr = inet->inet_saddr; ipc.opt = NULL; ipc.tx_flags = 0; ipc.ttl = 0; ipc.tos = -1; ipc.oif = sk->sk_bound_dev_if; if (msg->msg_controllen) { err = ip_cmsg_send(sk, msg, &ipc, false); if (unlikely(err)) { kfree(ipc.opt); goto out; } if (ipc.opt) free = 1; } saddr = ipc.addr; ipc.addr = daddr; if (!ipc.opt) { struct ip_options_rcu *inet_opt; rcu_read_lock(); inet_opt = rcu_dereference(inet->inet_opt); if (inet_opt) { memcpy(&opt_copy, inet_opt, sizeof(*inet_opt) + inet_opt->opt.optlen); ipc.opt = &opt_copy.opt; } rcu_read_unlock(); } if (ipc.opt) { err = -EINVAL; /* Linux does not mangle headers on raw sockets, * so that IP options + IP_HDRINCL is non-sense. * Linux 不会处理raw socket的头,所以IP 选项+ IP_HDRINCL没有意义 */ if (inet->hdrincl) goto done; if (ipc.opt->opt.srr) { if (!daddr) goto done; daddr = ipc.opt->opt.faddr; } } tos = get_rtconn_flags(&ipc, sk); if (msg->msg_flags & MSG_DONTROUTE) tos |= RTO_ONLINK; if (ipv4_is_multicast(daddr)) { if (!ipc.oif) ipc.oif = inet->mc_index; if (!saddr) saddr = inet->mc_addr; } else if (!ipc.oif) ipc.oif = inet->uc_index; flowi4_init_output(&fl4, ipc.oif, sk->sk_mark, tos, RT_SCOPE_UNIVERSE, inet->hdrincl ? IPPROTO_RAW : sk->sk_protocol, inet_sk_flowi_flags(sk) | (inet->hdrincl ? FLOWI_FLAG_KNOWN_NH : 0), daddr, saddr, 0, 0); if (!saddr && ipc.oif) { err = l3mdev_get_saddr(net, ipc.oif, &fl4); if (err < 0) goto done; } if (!inet->hdrincl) { rfv.msg = msg; rfv.hlen = 0; err = raw_probe_proto_opt(&rfv, &fl4); if (err) goto done; } security_sk_classify_flow(sk, flowi4_to_flowi(&fl4)); rt = ip_route_output_flow(net, &fl4, sk); if (IS_ERR(rt)) { err = PTR_ERR(rt); rt = NULL; goto done; } err = -EACCES; if (rt->rt_flags & RTCF_BROADCAST && !sock_flag(sk, SOCK_BROADCAST)) goto done; if (msg->msg_flags & MSG_CONFIRM) goto do_confirm; back_from_confirm: if (inet->hdrincl) err = raw_send_hdrinc(sk, &fl4, msg, len, &rt, msg->msg_flags, &ipc.sockc); else { sock_tx_timestamp(sk, ipc.sockc.tsflags, &ipc.tx_flags); if (!ipc.addr) ipc.addr = fl4.daddr; lock_sock(sk); err = ip_append_data(sk, &fl4, raw_getfrag, &rfv, len, 0, &ipc, &rt, msg->msg_flags); if (err) ip_flush_pending_frames(sk); else if (!(msg->msg_flags & MSG_MORE)) { err = ip_push_pending_frames(sk, &fl4); if (err == -ENOBUFS && !inet->recverr) err = 0; } release_sock(sk); } done: if (free) kfree(ipc.opt); ip_rt_put(rt); out: if (err < 0) return err; return len; do_confirm: dst_confirm(&rt->dst); if (!(msg->msg_flags & MSG_PROBE) || len) goto back_from_confirm; err = 0; goto done; }
ip_append_data函数将许多小片的数据整合成一个足够大的报文发送出去,这可以稍微提高一些性能,但是如果是使用RAW IP的特殊协议,使
inet->hdrincl等于1,那么这表示应用程序不希望内核对报文做过多干涉而要求直接发送出去,于是使用
raw_send_hdrinc函数来完成。
相关文章推荐
- Linux 2.4.x内核中网络协议栈QoS模块(TC)的设计与实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- (笔记)Linux下网络编程,采用TCP协议实现的C/S架构
- 由PPPOE看Linux网络协议栈的实现
- linux网络协议栈分析笔记1-接入部分
- Linux网络安全技术与实现(第2版)第二章笔记
- [置顶] Linux协议栈代码阅读笔记(二)网络接口的配置
- 由PPPOE看Linux网络协议栈的实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- linux网络协议栈分析笔记3-网桥2
- linux网络协议栈分析笔记12-路由2-FIB1
- linux网络协议栈分析笔记5-IP层的处理1
- 由PPPOE看Linux网络协议栈的实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现
- linux网络协议栈分析笔记10-arp邻居子系统3
- linux网络协议栈分析笔记11-路由1-路由缓存
- LINUX 网络协议栈实现分析-SKBUFF 的实现
- Linux 2.4.x 网络协议栈QoS模块(TC)的设计与实现(转)