数据发送过程zz_第1页
数据发送过程zz_第2页
数据发送过程zz_第3页
数据发送过程zz_第4页
数据发送过程zz_第5页
免费预览已结束,剩余5页可下载查看

付费下载

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、socket数据发送过程zz本文在基于以下三个条件所写的:1) 0SI七层网络通信模型。2) 所阐述的函数是基于LinuX2.6.1内核。3) 在面向连接的通信协议TCP/IPV4的基础上。由于七层模型应用层,表示层,会话层,传输层,网络层,数据链路层,物理层可以简化 为以下五层结构应用层(Application Layer),传输层(Transport Layer), 网络层(Network Layer), 数据链路层(Data Link Layer), 物理层(Physical Layer). 其中 七层模型中的前三层都归结为五层结构中的应用层。为了简化讨论,本文主要从这 五层结构来探讨。

2、Layer 5 :应用层 Application Layer在TCP协议上,当通过三方握手建立了连接之后,就进入数据包的实质发送阶段, 在本文中以send命令来阐述。当通过send将数据包发送之后,glibc函数库会启 用另外一个其定义的别用名函数_libc_sendto(),该函数最后会间接执行到 sendto系统调用:inline_syscall#nr(name,args); #nr 说明是该系统调用带有 nr 个 args 参数sendto系统调用的参数值是 6,而name就是sendto从上面的分析可以看出glibc将要执行的下面一条语句是in li ne_syscall6( name

3、,arg1,arg2,arg3,arg4,arg5,arg6)在该函数中一段主要功能实现代码如下:_asmvolatile_("callsys#%0%1=%2%3%4%5%6%7%8":inline_syscall_r0_out_constraint(_sc_0),"=r"(_sc_19),"=r"(_sc_16),"=r"(_sc_17),"=r"(_sc_18),"=r"(_sc_20),"=r"(_sc_21):"0"(_sc_0

4、),"2"(_sc_16),"3"(_sc_17),"4"(_sc_18),"1"(_sc_19),"5"(_sc_20),"6"(_sc_21):inline_syscall_clobbers) ; _sc_ret=_sc_0,_sc_err=_sc_19 ;该代码采用了嵌入汇编详细介绍查阅嵌入汇编相关书籍,其中:_sc_0=sendto ;_sc_19-_sc_21 分别是 arg1-arg6 ;inline_syscall_r0_out_constraint:功能相当于

5、"=r",选用一个寄存器来存储输出变量。"0"-"6"分别是 %0-%6 代表 _SC_0-_SC_21接下来函数最终通过Linux中顶顶有名的INT 0X80陷入系统核心。具体的过程可 以参考内核相关书籍。下面是一个兄弟对INT 0X80的简要介绍:在陷入系统内核以后,最终会调用系统所提供的系统调用函数sys_sendto(),该函数直接调用了 _sock_sendmsg(),该函数对进程做一个简单的权限检查之后就触 发套接字(socket)中定义的虚拟sendmsg的函数,进而进入到下一层传输层处理。Layer 4 : 传输层(T

6、ransport Layer)由上层的讨论可知,系统触发了sen dmsg虚拟接口函数,其实就是传输层中的tcp_sendmsg或是udp_sendmsg 看你所使用的协议而定。本文介绍tcp_se ndmsg().该函数需要做如下工作:1) 为sk_buff(后面简称skb)分配空间,该函数首先尝试在套接字缓冲队列中寻找 空闲空间,如果找不到就使用tcp_alloc_pskb()为其重新分配空间。2) 下面这步就会tcp_sendmsg函数的主要部分了,将数据拷贝到缓冲区。它分为如 下两种情况:2.1) 如果skb还有剩余空间的话,就使用skb_add_data()来向skb尾部添加数据 包

7、。代码如下:if(skb_tailroom(skb)0)/*We have some space in skb head.Superb ! */if(copy skb_tailroom(skb)copy=skb_tailroom(skb) ;if(err=skb_add_data(skb,from,copy) ! =0 gotodo_fault ;2.2) 如果skb没有了可用空间,内核会使用 TCP_PAG宏来为发送的数据包分配一 个高速缓存页空间,当该页被正确地分配后就调用 Copy_from_user(to(page地 址),from(usr空间),n)将用户空间数据包复制到page所在

8、的地址空间。但是我们都知道数据包在协议层之间的传输是通过 skb的,难道将数据包复制到这 个新分配的page中,内核就可以去睡大觉了吗?当然不是!接下来内核就要来处理 这个问题了,那么怎样来处理呢?此时就需要使用到skb中的另外一个数据区struct skb_shared_info,但是该数据区在创建skb时是没有为其分配空间的,也就是说它开始纯粹就是个指针,而没有 具体的告诉它要指向什么地方。这时大家应该知道它可以指向什么地方了,对,就 是page!在内核中对这种情况的具体是通过fill_page_desc(structsk_buff*skb,i nt I,struct page*page,

9、i nt off,i nt size)来实现的,代码如下:statici nli nevoidfill_page_descstruct sk_buff*skb,i nt i,structpage*page,i nt off,i nt size)skb_frag_t*frag=&skb_sh in fo(skb)-fragsi;frag-page=page ; frag-page_offset=off ; frag-size=size ; skb_shinfo(skb)- nr_frags=i+1 ; 这里需要注意的是struct skb_shared_info 只能通过 skb_shi

10、nfo来获取,在该结构体中 skb_flag_t类型的flagsi就是具体指向page的数组。2.3) 至此skb数据包的装载工作算是结束了,接下来就需要做一些后续工作,包括 是否要分片,以及后来的TCP协议头的添加。先看在tcp_sendmsg()中的最后一个 重要函数tcp_push,它的调用格式如下:statici nli nevoidtcp_pushstruct sock*sk,struct tcp_opt*tp,i nt flags,i ntmss_now,int nonagle)细心的朋友会发现,在该函数中传输的竟然不是skb,而是一个名为sock的结构体,那这又是什么东东呢?个人

11、理解是它在顶层协议层之间例 如:应用层和传输层之间的传输起着非常重要的作用,相当于沟通两层之间的纽 带。再深入查找下该结构体的构成,我们很容易发现这样一个结构体变量:struct sk_buff_head,有名称我们可以知道它是用来描述 skb头部信息的一个结构体,它 指向了 buffer的数据区。这下我们也明白了点,这个结构体其实还充当了一个队 列作用,是用来存储skb的数据区。协议层之间传输完之后,具体到该层处理时内 核就会从sk_buff_head逐个中取出skb数据区来处理,例如添加协议头等。好了,tcp_sendmsg到此结束了它的使命了,下面将要需要的一个函数就是在 tcp_pus

12、h()中直接用到的一个函数:_tcp_push_pending_frames(), 该函数又直接 调用tcp_write_xmit()函数来进一步对数据包处理,它包括一下两步:1) 检查是否需要对数据包进行分片,条件是只要skb中全部数据长度大于当前路由负荷量就需要分片。2) 采用 skb_clone(skb,GFP_ATOMIC)为 TCP_HEA分配一个 sk_buff 空间,这里需要注意的是skb_clone分配空间的特点,它首先是依照参数 skb来来复制出一个新 的sk_buff,新的skb和旧的skb共享数据变量缓存区,但是结构体缓冲区不是共 享的,这似乎和copy on write

13、机制有些相似。3) 在分配了一个新的skb之后,内核就会执行tcp_transmit_skb(). 其实内核中是 将2,3步合在一起的,如下:tcp_tra nsmit_skb(sk,skb_clo ne(skb,GFP_ATOMIC)接下来就是tcp_transmit_skb 函数的实现过程了。1) 通过skb_push()在skb前面加入tcp协议头信息。这包括序列号,源地址,目 的地址,校验和等。2) 通过tcp_opt结构体它是在该函数的开始部分从 sock结构体中获得的tcp_func结构体中的.queue_xmit虚拟功能函数,在IPV4中是调用了 ip_queue_xmit(),

14、这 样就进入了下一层二网络层。一 一Layer 3 网络层(Network Layer)在ip_queue_xmit()函数中需要做的事情有一下几件:1) 是否需要将数据包进行路由,如果需要的话就跳到包路由子程序段。判断是否需 要路由是由如下语句执行的:rt=(struct rtable*)skb-dst ; if(rt ! =NULL)goto packet_routed ;在 skb 的 dst变量中指明发送目标地址。它存放了路由路径中的下台主机地址。如果是需要对数据包进行路由,那么其执行分如下步骤1.1) 使用skb_push()在skb前面插入一段ip_headsize 大小的空间。1

15、.2) 填写ip协议头,包括ttl,protocol 等1.3) 写入校验和,最后调用 NF_HOO宏,关于NF_HOO后面介绍。调用的NF_HOOK 宏语句如下:NF_HOOK(PF_INET,NF_IP_LOCAL_OUT,skb,NULL,rt-u.dst.dev,dst_output) ; 2)如果没有路由地址,内核会尝试从外部可选项中来获取该地址,此时传输层发现没有 路由地址会不断地发出重发机制,直到路由地址获取到。当获取到路由地址之后, 内核会通过以下语句重新将地址赋给 skb-dst.之后就会进入到1)所述的路由子程 序段执行。skb-dst=dst_c lone(& r

16、t-u.dst);所以这样看来正常情况下内核都会进入1.3)所阐述的NF_HOO宏的执行。关于NF_HOO宏,我也不怎么了解,但是查了下内核后可以大体的知道,当二维数 组nf_hookspfhook( 其下标分别是调用宏中的第一个和第二个参数中定义了需 要的钩子函数时,就会调用nf_hook_slow函数来处理,如果没有定义钩子函数就 直接调用NF_HOO中的最后一个参数所指向的函数,在这里是:dst_output(skb)。在网上搜了下,发现一篇讲解 NF_HOO的帖子,很详细,链接 如下:上面已经谈到,当存在钩子函数时,内核转向nf_hook_slow函数来处理。下面阐述下这个函数:1)检

17、查hook函数是否真的已经设置,如果没有设置就将hook对应位通过移位来设置;当确认已经设置后就取出该钩子函数,如下:elem=&nf_hookspfhook; 2)执行 nf_iterate()函数,该函数采用list_for_each_continue_rcu()HOOK链表中的每个 nf_hook_ops 钩子结构体,通过其内部变量priority来判断它的优先级是否大于系统所定义的INT_MIN,如果小于就继续搜索,否则就执行该结构体单元中所指向的钩子函数。if(hook_thresh elem-priority)continue; /*Optimization: we don

18、't need tohold module refere nee here,s ince fun cti on can't sleep.-RR*/switch(elem- hook(hook,skb,indev,outdev,okfn)。当钩子函数成功执行之后,它会返回一个NF_ACCEP标志,3) 判断nf_iterate()函数的返回标志,如下:switch(verdict)case NF_ACCEPT : ret=okfn(skb) ; break ; case NF_DROP kfree_skb(skb) ; ret=-EPERM break ; 由上面的代码可以看到,

19、当标志是 NF_ACCEP时,内核会继续调用okfn(skb)函数,也就是传递给NF_HOO的最后一 个参数dst_output(skb)。该函数非常简单,就是间接启用和skb相关的output函数,如下:for( ; ; )err=skb-dst-output(skb) ; if(likely(err=O)return err;if(unlikely(err ! =NET_XMIT_BYPASS)return err ; 内核这句 skb-dst- output(skb),就将skb打入到了下面的一层-数据链路层.Layer 2 数据链路层(Data Link Layer)上层的output

20、函数最终会触发链路层中的 dev_queue_xmit(skb)函数。在该函数 中需要做的事情如下:1) 对传输过来的skb包进行检查,主要是:1.1) 数据包有分散的数据片段即skb_info(skb)-nr_frags 0),但是接口不能传输这样的数据包片段即dev-features 中没有设置NETIF_F_FRAGLIST)这个时候内 核就会执行数据包线性化函数_skb_linearize(skb,GFP_ATOMIC),简单来说该函 数就是将skb中的数据片段存储到由内核所创建的一个缓冲区中,并释放掉原来的 skb数据区,将skb指向新分配的数据缓冲区。1.2) 和上面的条件很相似,

21、不过还添加了一个判断条件,那就是设备是否在高内存 缓冲区,并且设备又不支持 DMA寸数据的存取,此时也需要将数据包线性化。2) 如果包没有实现IP校验,就需要再次对数据包检验。3) 启用qdisc_run(dev),该函数检查网卡是否可以接收数据,如果不可以就重新检查直到可以发送为止,如果可以就调用qdisc_restart() 来具体实现。qdisc_restart()的实现如下:3.1) 检查dev中数据包队列是否为空,如果不为空就试图获取驱动程序的使用权限,当网卡可以接收数据包时就调用dev-hard_start_xmit(skb,dev)来执行驱动程序的数据包发送函数。3.2) 如果没

22、有获取到驱动程序的使用权限,这中情况一般是在调用hard_start_xmit(skb,dev)时出现了暂时的配置错误。这时可以检查下驱动程序在被什么使用,如果是死循环的话,将数据包丢弃!3.3) 执行netif_schedule(dev),在该函数之后的情况我就不再多说了,有一个网 友写的很精彩,链接如下:至此,各协议层的数据包发送过程就算是全部完成了,接下来就进入到驱动程序的 详细介绍。网卡底层驱动开发1) 驱动模块的加载modulenit(fn)在驱动的开发之中,大家都知道是从moduleni t(fn)开始的,该内核宏允许你添加自定义的初始化函数。这里稍微扯远点,看下modulenit

23、是如何在内核中实现的,展开如下:#define module_init( x) _initcall( x&#41 ; #define_initcall( fn&#41 ; devicenitcall( fn&#41 ; #definedevicenitcall( fn&#41 ; _define_initcall( "6",fn&#41 ; #define_define_initcall( level,fn&

24、amp;#41 ; static initcall_t _initcall_#fn_attribute used attribute_( ( _section_&#40 ;".initcall"level".init") ) ) =fn虽然很长,其实就是做了一件事情,说明了系统最终调用的初始化函数为initcall_t _initcall_#fn(#fn用fn代替即可,在内核启动的过程中do_ini tcalls函数会调用该初始化函数。当然在以上宏定义中还给出一些关于

25、初始化函数的其它信息:_attribute used attribute_( ( _section_&#40 ; ".initcall"这里的_attribute_ 、used、_section_ 都是GNU编译器的保留字。_attribute_:表示属性,也就是赋予它所修饰的变量或函数后面指定的属性;used:表示该变量或函数代码的执行过程中会被用到;_section_&#40 ;".i ni tcall":指将其所修饰的变量或函数编译进.ini tcall 段。那么initcall段的地址在什么地方呢?

26、它包括在_initcall_start至U_initcall_end区间里,在 arch/i386/kernel/vmlinux.lds.S中找到可以找到该变量。好了,言归正传。接下来内核将要调用初始化函数fn 了。2) 初始化函数fn,在该函数中需要做的事情如下:2.1) 为对应的网络设备例如:ether,Wlan等分配net_device结构体 (alloc_netdev 或是alloc_etherdev),关于这两个函数其实很相似,后者也是直 接调用了 alloc_netdev 最终实现。不同之处在于:后者使调用alloc_netdev 时使用了内核所提供的初始化函数 ether_set

27、up ;前者使用的是程序员自定义的函数 xxx_setupalloc_netdev的实现代码如下:alloc_size=sizeof(*dev)+sizeof_priv+31 ; dev=(struct net_device*)kmalloc(alloc_size,GFP_KERNEL) ; if(dev=NULL)pri ntk(KERN_ERR"alloc_dev : Un able to allocate device memory.'n") ; return NULL ; memset(dev,0,alloc_size) ; if(sizeof_priv)d

28、ev- priv=(void*)(long)(dev+1)+31)&31); setup(dev) ; strcpy(dev-name,mask);从代码可以看出alloc_netdev函数间接调用了上层函数所提供的 setup函数来初始化dev结构体。到此大家都知道了初始化函数中包括的是打开, 关闭设备,传输函数等各个主要变量的初始化。2.2) 通过register_netdev(dev)来注册一个已经初始化好了的net_device设备。其实注册设备就是将dev链接到内核中的netdev的链表之中。当使用ifconfig 来为一个网络设备配置地址时,内核ioctl函数就会设置dev

29、- flag中的IFF_UP标志以打开接口,当IFF_UP设置之后,内核就将调用open函 数。3) 接口的打开接口打开函数中需要做的工作如下:3.1) 设置MAC地址,一般来说接口是不支持硬件地址改变的,所以就没有必要自定 义MAC地址设置函数,而只需要采用默认设置。默认设置是在 eth_setup()函数中 赋予的,就是将dev-set_mac_address设置为eth_mac_add(),该函数首先会判断 接口是否在工作,只有不在工作时才会启用设置命令memcpy(),如下所示:struct sockaddr*addr=p ; if(netif_running(dev)return-E

30、BUSY ; memcpy(dev- dev_addr,addr-sa_data,dev-addr_len); 3.2)必要时使用端口申请request_region()为什么说是必要时呢?因为端口申请的目的就是使得进程能够独 享10端口访问权限,不至于出现资源争用。但是当你能确定10端口只是被单个进程使用时,就可以省去该步骤。但是为了程序的健壮性考虑,还是加上这个函数为 妙。下面简单介绍下request_region(),他的调用格式如下:requset_regio n(start,size, name)调用该函数时时确定start-start+size地址空间是否可以使用,当确认可以使用之

31、后就调用端口读写函数来对端口进行访问。一些端口读写函数如下:void in sb( un sig ned port,void*addr,u nsig ned long count);void outsb( un sig ned port,void*addr,u nsig ned long count);读或写从内存地址addr开始的count字节.数据读自或者写入单个port端口.void in sw( un sig ned port,void*addr,u nsig ned long count);void outsw( un sig ned port,void*addr,u nsig ne

32、d long count);读或写16-位值到一个单个16-位端口 .void in sl( un sig ned port,void*addr,u nsig ned long count);void outsl( un sig ned port,void*addr,u nsig ned long count);读或写32-位值到一个单个32-位端口 .还补充一点:start的值是在驱动程序中赋予的,一般驱动程序都会定义一个ion,接着通过MODULE_PAR将其输出,这样在用户在通过insmod加载模块时 可以将该参数传递进来。3.3) 中断申请中断申请使用的时request_irq()函数。该函数的调用形式如下:int request_irq (un sig ned int irq,void(*ha ndler)(i nt irq,void*dev_id,struct pt_regs*regs)irq是要申请的硬件中断号。在In te

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论