你必须非常努力,才能看起来毫不费力!
分类: 网络与安全
2009-01-07 15:22:54
网络发生过程:
本文在基于以下三个条件所写的:
1) OSI七层网络通信模型。
2) 所阐述的函数是基于Linux2.6.1内核。
3) 在面向连接的通信协议TCP/IPV4的基础上。
由于七层模型(应用层, 表示层, 会话层, 传输层, 网络层, 数据链路层, 物理层)可以简化为以下五层结构: 应用层(Application Layer), 传输层(Transport Layer), 网络层(Network Layer), 数据链路层(Data Link Layer), 物理层(Physical Layer).其中七层模型中的前三层都归结为五层结构中的应用层。为了简化讨论,本文主要从这五层结构来探讨。
Layer 5:应用层(Application Layer)
在TCP协议上,当通过三方握手建立了连接之后,就进入数据包的实质发送阶段,在本文中以send命令来阐述。当通过send将数据包发送之后,glibc函数库会启用另外一个其定义的别用名函数__libc_sendto(),该函数最后会间接执行到sendto系统调用:
inline_syscall##nr(name, args);// ##nr说明是该系统调用带有nr个args参数sendto系统调用的参数值是6,而name就是sendto
从上面的分析可以看出glibc将要执行的下面一条语句是
inline_syscall6(name,arg1,arg2,arg3,arg4,arg5,arg6)
在该函数中一段主要功能实现代码如下:
__asm__ __volatile__ \
("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), "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:功能相当于"=r",选用一个寄存器来存储输出变量。
"0"--"6"分别是%0--%6,代表_sc_0--_sc_21
接下来函数最终通过Linux中顶顶有名的INT 0X80陷入系统核心。具体的过程可以参考内核相关书籍。下面是一个兄弟对INT 0X80的简要介绍:
http://blog.chinaunix.net/u2/65427/showart_712571.html
在陷入系统内核以后,最终会调用系统所提供的系统调用函数sys_sendto(),该函数直接调用了__sock_sendmsg(),该函数对进程做一个简单的权限检查之后就触发套接字(socket)中定义的虚拟sendmsg的函数,进而进入到下一层传输层处理。
Layer 4: 传输层(Transport Layer)
由上层的讨论可知,系统触发了sendmsg虚拟接口函数,其实就是传输层中的tcp_sendmsg或是udp_sendmsg,看你所使用的协议而定。本文介绍tcp_sendmsg().
该函数需要做如下工作:
1)为sk_buff(后面简称skb)分配空间,该函数首先尝试在套接字缓冲队列中寻找空闲空间,如果找不到就使用tcp_alloc_pskb()为其重新分配空间。
2) 下面这步就会tcp_sendmsg函数的主要部分了,将数据拷贝到缓冲区。它分为如下两种情况:
2.1)如果skb还有剩余空间的话,就使用skb_add_data()来向skb尾部添加数据包。代码如下:
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)
goto do_fault;
}
2.2)如果skb没有了可用空间,内核会使用TCP_PAGE宏来为发送的数据包分配一个高速缓存页空间,当该页被正确地分配后就调用Copy_from_user(to(page地址),from(usr空间),n)将用户空间数据包复制到page所在的地址空间。
但是我们都知道数据包在协议层之间的传输是通过skb的,难道将数据包复制到这个新分配的page中,内核就可以去睡大觉了吗?当然不是!接下来内核就要来处理这个问题了,那么怎样来处理呢?
此时就需要使用到skb中的另外一个数据区struct skb_shared_info[],但是该数据区在创建skb时是没有为其分配空间的,也就是说它开始纯粹就是个指针,而没有具体的告诉它要指向什么地方。这时大家应该知道它可以指向什么地方了,对,就是page!在内核中对这种情况的具体是通过fill_page_desc(struct sk_buff *skb,int I,struct page *page,int off,int size)来实现的,代码如下:
static inline void fill_page_desc(struct sk_buff *skb, int i, struct page *page, int off, int size){ skb_frag_t *frag = &skb_shinfo(skb)->frags[i]; frag->page = page; frag->page_offset = off; frag->size = size; skb_shinfo(skb)->nr_frags = i + 1;}这里需要注意的是struct skb_shared_info[]只能通过skb_shinfo来获取,在该结构体中skb_flag_t类型的flags[i]就是具体指向page的数组。
2.3)至此skb数据包的装载工作算是结束了,接下来就需要做一些后续工作,包括是否要分片,以及后来的TCP协议头的添加。先看在tcp_sendmsg()中的最后一个重要函数tcp_push,它的调用格式如下:
static inline void tcp_push(struct sock *sk, struct tcp_opt *tp, int flags, int mss_now, int nonagle)细心的朋友会发现,在该函数中传输的竟然不是skb,而是一个名为sock的结构体,那这又是什么东东呢?个人理解是它在顶层协议层之间(例如:应用层和传输层之间)的传输起着非常重要的作用,相当于沟通两层之间的纽带。再深入查找下该结构体的构成,我们很容易发现这样一个结构体变量:struct sk_buff_head,有名称我们可以知道它是用来描述skb头部信息的一个结构体,它指向了buffer的数据区。这下我们也明白了点,这个结构体其实还充当了一个队列作用,是用来存储skb的数据区。协议层之间传输完之后,具体到该层处理时内核就会从sk_buff_head逐个中取出skb数据区来处理,例如添加协议头等。
好了,tcp_sendmsg到此结束了它的使命了,下面将要需要的一个函数就是在tcp_push()中直接用到的一个函数:__tcp_push_pending_frames(),该函数又直接调用tcp_write_xmit()函数来进一步对数据包处理,它包括一下两步:
1)检查是否需要对数据包进行分片,条件是只要skb中全部数据长度大于当前路由负荷量就需要分片。
2) 采用skb_clone(skb,GFP_ATOMIC)为TCP_HEAD分配一个sk_buff空间,这里需要注意的是skb_clone分配空间的特点,它首先是依照参数skb来来复制出一个新的sk_buff,新的skb和旧的skb共享数据变量缓存区,但是结构体缓冲区不是共享的,这似乎和copy on write机制有些相似。
3) 在分配了一个新的skb之后,内核就会执行tcp_transmit_skb().其实内核中是将2,3步合在一起的,如下:
tcp_transmit_skb(sk, skb_clone(skb, GFP_ATOMIC))
接下来就是tcp_transmit_skb函数的实现过程了。
1) 通过skb_push()在skb前面加入tcp协议头信息。这包括序列号,源地址,目的地址,校验和等。
2) 通过tcp_opt结构体(它是在该函数的开始部分从sock结构体中获得的)来访问tcp_func结构体中的.queue_xmit虚拟功能函数,在IPV4中是调用了ip_queue_xmit(),这样就进入了下一层——网络层。
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.2) 填写ip协议头,包括ttl,protocol等
1.3) 写入校验和,最后调用NF_HOOK宏,关于NF_HOOK后面介绍。调用的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_clone(&rt->u.dst);
所以这样看来正常情况下内核都会进入1.3)所阐述的NF_HOOK宏的执行。
关于NF_HOOK宏,我也不怎么了解,但是查了下内核后可以大体的知道,当二维数组nf_hooks[pf][hook](其下标分别是调用宏中的第一个和第二个参数)中定义了需要的钩子函数时,就会调用nf_hook_slow函数来处理,如果没有定义钩子函数就直接调用NF_HOOK中的最后一个参数所指向的函数,在这里是:dst_output(skb)。在网上搜了下,发现一篇讲解NF_HOOK的帖子,很详细,链接如下:
上面已经谈到,当存在钩子函数时,内核转向nf_hook_slow函数来处理。下面阐述下这个函数:
1) 检查hook函数是否真的已经设置,如果没有设置就将hook对应位通过移位来设置;当确认已经设置后就取出该钩子函数,如下:
elem = &nf_hooks[pf][hook];2) 执行nf_iterate()函数,该函数采用list_for_each_continue_rcu()来搜索HOOK链表中的每个nf_hook_ops钩子结构体,通过其内部变量priority来判断它的优先级是否大于系统所定义的INT_MIN,如果小于就继续搜索,否则就执行该结构体单元中所指向的钩子函数。
if (hook_thresh > elem->priority) continue;/* Optimization: we don't need to hold module
reference here, since function can't sleep. --RR */ switch (elem->hook(hook, skb, indev, outdev, okfn)) {。。。}当钩子函数成功执行之后,它会返回一个NF_ACCEPT标志,
3) 判断nf_iterate()函数的返回标志,如下:
switch (verdict) { case NF_ACCEPT: ret = okfn(skb); break; case NF_DROP: kfree_skb(skb); ret = -EPERM; break; }由上面的代码可以看到,当标志是NF_ACCEPT时,内核会继续调用okfn(skb)函数,也就是传递给NF_HOOK的最后一个参数dst_output(skb)。该函数非常简单,就是间接启用和skb相关的output函数,如下:
for (;;) { err = skb->dst->output(skb); if (likely(err == 0)) return err; if (unlikely(err != NET_XMIT_BYPASS)) return err; }内核这句skb->dst->output(skb),就将skb打入到了下面的一层---数据链路层.
Layer 2:数据链路层(Data Link Layer)
上层的output函数最终会触发链路层中的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) 和上面的条件很相似,不过还添加了一个判断条件,那就是设备是否在高内存缓冲区,并且设备又不支持DMA对数据的存取,此时也需要将数据包线性化。
2) 如果包没有实现IP校验,就需要再次对数据包检验。
3) 启用qdisc_run(dev),该函数检查网卡是否可以接收数据,如果不可以就重新检查直到可以发送为止,如果可以就调用qdisc_restart()来具体实现。qdisc_restart()的实现如下:
3.1)检查dev中数据包队列是否为空,如果不为空就试图获取驱动程序的使用权限,当网卡可以接收数据包时就调用dev->hard_start_xmit(skb, dev)来执行驱动程序的数据包发送函数。
3.2)如果没有获取到驱动程序的使用权限,这中情况一般是在调用hard_start_xmit(skb,dev)时出现了暂时的配置错误。这时可以检查下驱动程序在被什么使用,如果是死循环的话,将数据包丢弃!
3.3)执行netif_schedule(dev),在该函数之后的情况我就不再多说了,有一个网友写的很精彩,链接如下:
=
至此,各协议层的数据包发送过程就算是全部完成了,接下来就进入到驱动程序的详细介绍。
网卡底层驱动开发
1)驱动模块的加载module_init(fn)
在驱动的开发之中,大家都知道是从module_init(fn)开始的,该内核宏允许你添加自定义的初始化函数。这里稍微扯远点,看下module_init是如何在内核中实现的,展开如下:
#define module_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall("6",fn)
#define __define_initcall(level, fn) \
static initcall_t __initcall_##fn __attribute_used__ \
__attribute__((__section__(".initcall" level ".init"))) = fn
虽然很长,其实就是做了一件事情,说明了系统最终调用的初始化函数为initcall_t __initcall_##fn(##fn用fn代替即可),在内核启动的过程中do_initcalls函数会调用该初始化函数。当然在以上宏定义中还给出一些关于初始化函数的其它信息:
__attribute_used__ \
__attribute__((__section__(".initcall"
这里的__attribute__、used、__section__都是GNU编译器的保留字。
__attribute__:表示属性,也就是赋予它所修饰的变量或函数后面指定的属性;
used:表示该变量或函数代码的执行过程中会被用到;
__section__(".initcall":指将其所修饰的变量或函数编译进. initcall段。
那么initcall段的地址在什么地方呢?它包括在__initcall_start到__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_setup;前者使用的是程序员自定义的函数xxx_setup。关于alloc_netdev的实现代码如下:
alloc_size = sizeof (*dev) + sizeof_priv + 31; dev = (struct net_device *) kmalloc (alloc_size, GFP_KERNEL); if (dev == NULL) { printk(KERN_ERR "alloc_dev: Unable to allocate device memory.\n"); return NULL; } memset(dev, 0, alloc_size); if (sizeof_priv) dev->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->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 -EBUSY; memcpy(dev->dev_addr, addr->sa_data,dev->addr_len);3.2)必要时使用端口申请request_region().为什么说是必要时呢?因为端口申请的目的就是使得进程能够独享IO端口访问权限,不至于出现资源争用。但是当你能确定IO端口只是被单个进程使用时,就可以省去该步骤。但是为了程序的健壮性考虑,还是加上这个函数为妙。下面简单介绍下request_region(),他的调用格式如下:
requset_region(start, size, name)
调用该函数时时确定start---start+size地址空间是否可以使用,当确认可以使用之后就调用端口读写函数来对端口进行访问。一些端口读写函数如下:
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
读或写从内存地址addr 开始的count 字节. 数据读自或者写入单个 port 端口.
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
读或写 16-位值到一个单个16-位 端口.
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
读或写 32-位值到一个单个32-位 端口.
还补充一点:start的值是在驱动程序中赋予的,一般驱动程序都会定义一个io[n],接着通过MODULE_PARM()将其输出,这样在用户在通过insmod加载模块时可以将该参数传递进来。
3.3)中断申请
中断申请使用的时request_irq()函数。该函数的调用形式如下:
int request_irq(unsigned int irq, void (*handler)(int irq, void *dev_id, struct pt_regs *regs )
irq是要申请的硬件中断号。在Intel平台,范围0--15。
handler是向系统登记的中断处理函数。这是一个回调函数,中断发生时,系统调用这个函数,传入的参数包括硬件中断号,device id,寄存器值。
dev_id就是下面的request_irq时传递给系统的参数dev_id。
irqflags是中断处理的一些属性。比较重要的有SA_INTERRUPT,
标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT)。快速处理程序被调用时屏蔽所有中断。慢速处理程序不屏蔽。还有一个SA_SHIRQ属性,设置了以后运行多个设备共享中断。dev_id在中断共享时会用到。一般设置这个设备为device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用irq2dev_map找到中断对应的设备。
在该函数的具体实现中就是先用kmalloc()来分配一个struct irqaction,用所传递过来的参数填充该结构体中的相应值,最后将该结构体压入系统中断队列之中。
4) 数据的发送
讲到数据的发送,我们就不得不稍微再回到前面一点:关于IO端口的分配。
当IO端口分配成功之后,就需要使用该IO端口来访问设备存储空间,提前为数据包的发送来建立缓冲区。那么这个缓冲区的建立过程又是怎样的呢?
首先通过inw/outw(当然这要看你分配的IO端口是多少位的了),来分别向网卡控制寄存器读出状态/写入命令。当控制器命令成功写入之后,就需要做一个简单的检查,从寄存器读取状态值来判断缓冲区空间是否已经分配好了。好了,当缓冲区分配好了之后,我们就进入正式的数据发送的讨论。
4.1)在驱动程序的开始就定义一个描述你所使用卡的结构体,之后就使用dev->priv来初始化该数据结构,之后的传输过程就主要靠你自定义的这个数据结构来完成了。
4.2)调用正式的数据发送函数
4.2.1)该函数首先对数据包头类型进行区分,并赋予相应的数据包头的大小。
4.2.2)查询上面所阐述的数据缓冲区是否空闲,如果空闲就将数据打入到网卡所分配的数据缓冲中,这个打入过程是通过outw/outsb来完成的(即由内存向某以端口地址写入size大小的数据量)。
4.2.3)设置硬件发送命令,将该命令写入到网卡数据发送控制寄存器中,启动网卡发送。
4.2.4)判断数据是否发送成功。
这样数据包在链路层和物理层的发送工作就算是全部完成了!
网络接收过程:
Layer 2:数据链路层(Data Link Layer)
在进入正式讨论数据包的接收之前,需要介绍一下linux中断过程。
当网卡检测到一个数据包到来时,就会向8259A触发相应的中断信号线,识别为一个中断后,控制单元将会执行如下步骤:
1. 确定与中断或异常关联的向量i(0≤ i ≤255)
2. 读由idtr寄存器指向的IDT表中的第i项。
3. 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符标识的段描述符。这个描述符指定中断或异常处理程序所在的段的基地址。
4. 确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较。如果CPL小于DPL,就产生一个“通常保护”异常,因为中断处理程序的特权级不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“通常保护”异常,这最后
一个检查可以避免用户应用程序访问特殊的陷阱门和中断门。
5. 检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈,通过执行以下步骤来保证这一点:
A. 读tr寄存器,以访问运行进程的TSS段。
B. 用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到。
C. 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
6. 如果故障已发生,用引起异常的指令地址装载cs和eip寄存器,从而使得这条指令能再次被执行。
7. 在栈中保存eflag、cs和eip的内容。
8. 如果异常产生了一个硬件出错码,则将它保存在栈中。
9. 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
从上面的步骤可以看出,硬件会做相应的一些环境保存:转入到 SS和ESP指针所在的地址,也就是新栈地址。这样就可以访问新的栈了(通过SS+ESP),但是还有一个问题:当我们的中断返回时也要恢复到原来的栈,那么原来所在的栈都保存到什么地方呢?其实就是保存在所切换到的这个新栈中,好了地方是找到了,保存到新栈中。那么旧的SS和ESP的值又到哪里去找呢?是在TSS中,其实在中断发生的时候,如果检测到运行级别发生了改了,将寄存器SS,ESP中的值保存进TSS的相应级别位置,再加载新的SS,ESP的值,然后从TSS中取出旧的SS,ESP值,再压栈.压栈前后示意图如下:
以上所陈述的都是硬件自动保存的环境,那么还有很多寄存器的值由谁来保存呢?这就需要操作系统来完成了。
从上面的第9步,我们可以清晰的看到,下一步CPU将要做的事情就是执行中断或是异常所在的地址。其实IRQn中断处理程序所在的地址开始是保存在interrupt[n]之中的,之后才复制到IDT相应的表项中断门中.那么我们可以看下iterript[n]是在什么地方,是否在它后面还有些什么代码?在/arch/i386/kernel/entry.S中可以找到这个地址:
.data
ENTRY(interrupt)
.text
vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS
ALIGN
1: pushl $vector-256
jmp common_interrupt
.data
.long 1b
.text
vector=vector+1
.endr
可以看到在interrupt之后还出现了这么两句:
1: pushl $vector-256//将中断索引号取负压栈
jmp common_interrupt
好,接下来看common_interrupt是什么?
ALIGN
common_interrupt:
SAVE_ALL
movl %esp,%eax
call do_IRQ
jmp ret_from_intr
在common_interrupt:之后首先我们看到的就是一个宏SAVE_ALL,该宏就是我们刚才所说的系统对其它一些中断环境的保存,如下:
#define SAVE_ALL \
__SAVE_ALL; \
__SWITCH_KERNELSPACE; #在没有定义CONFIG_X86_HIGH_ENTRY的情况下,此宏是一个空宏
__SAVE_ALL定义如下:
#define __SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es;
__USER_DS的设置是为了在中断返回之后,防止不法程序对内核的访问而设置的,也免去了内核清除寄存器的任务,使得系统提高了效率。从eax开始到ebx都是传递给中断处理函数的参数选项。
以上其实都是我对网络上一篇非常经典的关于中断初始化的文章<<linux中断处理之初始化>>的一个简述,这篇文章的完整内容如下:
http://blog.chinaunix.net/u/26185/showart_1405389.html
好了,接下来就是进入do_IRQ了。关于d0_IRQ()所做的工作,以及后来如何调用到我们所定义的中断处理函数的详细过程可以参考下面这篇文章(这篇文章太经典了!),名为Linux处理之IRQ中断。
好了,关于系统中断我们先简单介绍到这里,下面进入Linux驱动开发中所对应的中断处理程序。
1) 中断处理函数
由前面的分析可以看到内核接下来就进入到了中断处理部分。中断处理函数主要做如下工作:
1.1) 间接调用outsw来访问网卡控制寄存器中的接收标志以及其它一些硬件标志,做出相应的动作。当检测到网卡中数据包到来了时就进入数据包的处理阶段。
1.2) 数据包处理阶段:
1.2.1)首先访问网卡控制寄存器,看数据包被放在了网卡的什么地方,返回一个缓冲区的标识号(index)。
1.2.2)查询该标识号所对应的缓冲区是否空闲.
1.2.3) 从网卡所标识的缓冲区中将数据读出到内存缓冲区中。
1.2.4)判断所接收到的数据大小,如果太大和太小均会被丢弃。如下:
if (len > 2312) {
printk( KERN_ERR "card_name: Bad size %d\n", len );
goto badrx;
}
if (len == 0)
goto badrx;
1.2.5)判断数据帧类型,从而调整数据包头的大小,如下(针对无线网卡):
bap_read (apriv, (u16*)&fc, sizeof(fc), BAP0);
fc = le16_to_cpu(fc);
switch (fc & 0xc) {
case 4:
if ((fc & 0xe0) == 0xc0)
hdrlen = 10;
else
hdrlen = 16;
break;
case 8:
if ((fc&0x300)==0x300){
hdrlen = 30;
break;
}
default:
hdrlen = 24;
}
} else
hdrlen = ETH_ALEN * 2;
1.2.6)为sk_buff数据结构分配一个空间。
1.2.7)视数据包的大小,从而调整sk_buff的大小。之后将数据再次读入到sk_buff之中存储。
1.2.8)填写该数据结构中的相应项,如下:
skb->mac.raw = skb->data;
skb->pkt_type = PACKET_OTHERHOST;
skb->dev = apriv->wifidev;
skb->protocol = htons(ETH_P_802_2);
skb->dev->last_rx = jiffies;
skb->ip_summed = CHECKSUM_NONE;
1.2.9)最后一步,调用netif_rx( skb ),将数据交到上层处理。
2)netif_rx()
该函数首先检测所传送过来的skb中的时间戳是否有值,没有的话就获取当前时间作为时间戳。接下来询问当前队列是否还有空闲,如果由空闲就将skb压入到struct softnet_data queue结构体的队列之中,如下:
__skb_queue_tail(&queue->input_pkt_queue, skb);
最后就调用netif_rx_schedule函数,如下:
netif_rx_schedule (&queue->backlog_dev);这里需要注意的是这个函数的参数,看下struct softnet_data的具体结构,如下:struct softnet_data采用该数据结构的原因是当数据包到来时就将它们挂在这个这个数据结构上,这样访问这些数据时就不需要锁定了。
可以看到backlog_dev是一个struct net_device结构,所以数据包就转化为了该数据结构来传输。
2) netif_rx_schedule()
该函数会去调用__netif_rx_schedule,在函数__netif_rx_schedule中会去触发软中断NET_RX_SOFTIRQ, 也即是去调用net_rx_action.如下:
__raise_softirq_irqoff(NET_RX_SOFTIRQ);//触发软中断在net_rx_action函数中会去调用设备的poll函数, 它是设备自己注册的.在设备的poll函数中, 会去调用netif_receive_skb函数。
3) netif_receive_skb(skb)
关于这个函数,R.wen ()写了一份介绍链路层收发数据包的详细文档,大家可以去看看。(接收过程中的第五点就是介绍这个函数的),地址如下:
=
Layer 3:网络层(Network Layer)
由以上文章的分析,我们可以看到。在数据链路层最后是内核调用了deliver_skb()来向上层(也就是网络层)中所注册的协议发送一份IP数据包的副本,当然再内核中可能会注册多个的协议(如ARP,IP),这里我们讲解IP协议,它具体的处理函数是ip_rcv().
1)ip_rcv()
1.1)该函数首先对传送过来的数据包中取出ip头描述数据结构struct iphdr *iph,如下:
iph = skb->nh.iph;1.3) 依据RFC1122中的规定,对不满足规定的IP头数据包丢弃,RFC1122中认为一个正常的数据包应该有如下几种:1. 数据包的大小不能小于IP头的大小;2. 版本是IPV4(当然,我们这是以IPV4来阐述的);3. 校验码正确;4. 数据包的长度是真实的。如下代码演示了以上规范:if (iph->ihl < 5 || iph->version != 4) goto inhdr_error;if (!pskb_may_pull(skb, iph->ihl*4)) goto inhdr_error;if (ip_fast_csum((u8 *)iph, iph->ihl) != 0) goto inhdr_error;if (skb->len < len || len < (iph->ihl<<2)) goto inhdr_error;如果一切都OK,那么就转入到钩子函数执行,如下:
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,ip_rcv_finish);再发送过程中,简单的介绍过钩子函数,这里就不再累述了。钩子函数最终会转到ip_rcv_finish()函数执行。接下来分析下看这个函数。2)ip_rcv_finish()到此,ip_rcv_finish函数真正要完成一些IP层的工作了。IP层要做的主要工作就是路由,要决定把数据包往那里送。路由的工作是通过函数ip_route_input()实现的。对于进来的包可能的路由有这些: 属于本地的数据(即是需要传递给TCP,UDP,IGMP这些上层协议的) ; 需要转发的数据包(网关或者NAT服务器之类的); 不可能路由的数据包(地址信息有误);具体所需要做的工作如下:
2.1)首先判断skb->dst是否等于NULL,也就是查看数据包中是否有目的地址。如果没有的话,该函数会调用ip_route_input()函数在内核的路由hash表中寻找一个目的地址,其主要代码如下:
int ip_route_input(struct sk_buff *skb, u32 daddr, u32 saddr, u8 tos, struct net_device *dev){ struct rtable * rth; unsigned hash; int iif = dev->ifindex; tos &= IPTOS_RT_MASK;hash = rt_hash_code(daddr, saddr ^ (iif << 5), tos);//计算hash值 rcu_read_lock(); for (rth = rt_hash_table[hash].chain; rth; rth = rth->u.rt_next) //在相//同的hash链表中搜索能满足条件的目的地址{ smp_read_barrier_depends(); if (rth->fl.fl4_dst == daddr && rth->fl.fl4_src == saddr && rth->fl.iif == iif && rth->fl.oif == 0 &&#ifdef CONFIG_IP_ROUTE_FWMARK
rth->fl.fl4_fwmark == skb->nfmark &&#endif
rth->fl.fl4_tos == tos) { rth->u.dst.lastuse = jiffies; dst_hold(&rth->u.dst); rth->u.dst.__use++; RT_CACHE_STAT_INC(in_hit); rcu_read_unlock(); skb->dst = (struct dst_entry*)rth;//找到之后赋予skb->dst return 0; }… … … …
return ip_route_input_slow(skb, daddr, saddr, tos, dev);
最后调用了ip_route_input_slow()函数,在这个函数中针对所传送过来的数据包类型分别进行了处理,我们这里要讨论的是本地数据包,也就是需要传送给TCP,UDP等这些协议的数据包。具体看这个子代码:
local_input: rth = dst_alloc(&ipv4_dst_ops); if (!rth) goto e_nobufs; rth->u.dst.output= ip_rt_bug; atomic_set(&rth->u.dst.__refcnt, 1); rth->u.dst.flags= DST_HOST; if (in_dev->cnf.no_policy) rth->u.dst.flags |= DST_NOPOLICY; rth->fl.fl4_dst = daddr; rth->rt_dst = daddr; rth->fl.fl4_tos = tos;#ifdef CONFIG_IP_ROUTE_FWMARK
rth->fl.fl4_fwmark= skb->nfmark;#endif
rth->fl.fl4_src = saddr; rth->rt_src = saddr;#ifdef CONFIG_IP_ROUTE_NAT
rth->rt_dst_map = fl.fl4_dst; rth->rt_src_map = fl.fl4_src;#endif
#ifdef CONFIG_NET_CLS_ROUTE
rth->u.dst.tclassid = itag;#endif
rth->rt_iif = rth->fl.iif = dev->ifindex; rth->u.dst.dev = &loopback_dev; dev_hold(rth->u.dst.dev); rth->rt_gateway = daddr; rth->rt_spec_dst= spec_dst; rth->u.dst.input= ip_local_deliver; rth->rt_flags = flags|RTCF_LOCAL; if (res.type == RTN_UNREACHABLE) { rth->u.dst.input= ip_error; rth->u.dst.error= -err; rth->rt_flags &= ~RTCF_LOCAL; } rth->rt_type = res.type; goto intern;首先看第1句,rth = dst_alloc(&ipv4_dst_ops);该语句是为struct dst_entry数据结构分配一个空间,之后就是对该数据结构进行填充,这里需要注意的是rth->u.dst.input= ip_local_deliver;该行对后面将要讲解的dst->input函数进行了填充,就是具体由ip_local_deliver函数来处理。接下来就看标记intern:intern: err = rt_intern_hash(hash, rth, (struct rtable**)&skb->dst);rt_intern_hash()主要是将rth加入到hash队列之中,之后将其赋给skb->dst,也就是在skb中添加了这样一个路由地址包了!2.2)由于接下来要在skb数据包上添加一个ip_option的数据结构,所以skb就需要在头部重新添加一个头部空间来加载这些数据量,如下:该函数的完整代码如下:
static inline int dst_input(struct sk_buff *skb){ int err; for (;;) { err = skb->dst->input(skb); if (likely(err == 0)) return err; /* Oh, Jamal... Seems, I will not forgive you this mess. :-) */ if (unlikely(err != NET_XMIT_BYPASS)) return err; }}代码功能很简单,就是不断地调用skb->dst->input(skb)函数,这个函数其实就是和我们在发送过程的网络层中最后所讨论的那个函数skb->dst->output(skb)是对应的。就是将数据包传送到上一层:传输层.具体的过程如下:由上面的讨论我们已经知道了,input函数实际上是由ip_local_deliver()来处理,在这个函数中调用了钩子函数NF_HOOK,最后会转到一个名为ip_local_diliver_finish()的函数来处理,在这个函数中我们只需要注意以下两行:if ((ipprot = inet_protos[hash]) != NULL) { int ret;… … … … ret = ipprot->handler(skb);在这两行代码中,我们注意到有个inet_protos,这是一个承载协议类型的数据结构,在ipv4中是如下定义的:static struct inet_protocol tcp_protocol = { .handler = tcp_v4_rcv, .err_handler = tcp_v4_err, .no_policy = 1,};到此,我们明白了,接下来在传输层中首先要执行的函数是tcp_v4_rcv()。Layer 4: 传输层(Transport Layer)
1) tcp_v4_rcv(skb)
该函数按如下步骤进行:
1.1) 获取tcp头数据结构,并且在skb->cb[]中添加数据包控制信息,如下:
th = skb->h.th; TCP_SKB_CB(skb)->seq = ntohl(th->seq); TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin + skb->len - th->doff * 4); TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq); TCP_SKB_CB(skb)->when = 0; TCP_SKB_CB(skb)->flags = skb->nh.iph->tos; TCP_SKB_CB(skb)->sacked = 0;以上的TCP_SKB_CB是获取skb->cb[]的宏。
1.2) 将skb中的源地址,目的地址,数据包指针都转存到一个sock的数据结构中,在发送过程中讲过这个数据结构,它是沟通传输层和协议层之间的重要数据类型。
sk = __tcp_v4_lookup(skb->nh.iph->saddr, th->source, skb->nh.iph->daddr, ntohs(th->dest), tcp_v4_iif(skb));1.4) 调用tcp_rcv_established
在该函数中会调用tcp_data_queue(),该函数主要是用来维护receive queue以及out_of_order_queue两个队列的。当用户空间正在等待内核的数据包时,内核就会调用如下函数将数据包从内核拷贝到用户空间skb_copy_datagram_iovec(),否则内核会将sk_buff挂载到socket的队列之中以等待用户需要时再进行拷贝。最后函数会调用sk_data_ready()函数来标明数据可以获取,同时也唤醒等待数据包的用户进程。
Layer 5:应用层(Application Layer)
最后一层就是和用户直接接触的一层。当用户调用read,recvfrom,recvmsg时都会陷入内核最后都调用__sock_recvmsg。该函数首先进行权限检查,当是内核权限时,该函数会调用下层(即传输层)套接字所定义的虚拟函数recvmsg,而这个函数针对不同的协议会定义不同的处理函数,在TCP中就是tcp_recvmsg()。该函数也是使用skb_copy_datagram_iovec()函数从套接字队列中将数据拷贝到用户空间,或是使用sk_wait_data()等待内核空间数据包的到来,这样就返到了上层的1.4)最后所阐述的,就是当数据包到来时内核会使用sk_data_ready()函数来标明数据可以获得,同时启动等待队列。到此数据包的接收就算是圆满完成了!