好文推荐:
全网最牛Linux内核剖析--IntelCPU体系结构
一文让你看懂Linux五大模块内核源码,内核整体构架设计(超详尽)
嵌入式前景真的好吗?那有点悬!
一文教你怎么使用GDB+Qemu调试Linux内核
Linux内核必看五本书籍(强烈推荐)
全网独一无二Linux内核Makefle系统文件解读(一)(纯文字代码)
带你深度了解Linux内核构架和工作原理!
怎么看懂GDB底层实现原理(从这几点入手~)
一文彻底理解Memorybarrier(显存屏障)
一篇文带你看懂,虚拟显存、内存分页、分段、段页式显存管理(超详尽)
int main(){
int serverSocketFd = socket(AF_INET, SOCK_DGRAM, 0);
bind(serverSocketFd, ...);
char buff[BUFFSIZE];
int readCount = recvfrom(serverSocketFd, buff, BUFFSIZE, 0, ...);
buff[readCount] = '';
一Linux网路收包总览
【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人认为比较好的学习书籍、视频资料共享在群文件上面linux内核中的串口驱动,有须要的可以自行添加哦!!!前100名进群发放,额外附送大厂笔试题。
学习直通车:
内核资料直通车:
二Linux启动2.1创建ksoftirqd内核线程
相关代码如下:
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",};
static __init int spawn_ksoftirqd(void){
register_cpu_notifier(&cpu_nfb);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
当ksoftirqd被创建下来之后,它还会步入自己的线程循环函数ksoftirqd_should_run和run_ksoftirqd了。不停地判定有没有软中断须要被处理。这儿须要注意的一点是,软中断不仅仅只有网路软中断,还有其它类型。
//file: include/linux/interrupt.h
enum{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ,
2.2网路子系统初始化
linux内核通过调用subsys_initcall来初始化各个子系统,在源代码目录里你可以grep出许多对这个函数的调用。这儿我们要说的是网路子系统的初始化,会执行到net_dev_init函数。
......
for_each_possible_cpu(i) {
struct softnet_data *sd = &per_cpu(softnet_data, i);
memset(sd, 0, sizeof(*sd));
skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
sd->completion_queue = NULL;
INIT_LIST_HEAD(&sd->poll_list);
......
}
......
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
在这个函数里,会为每位CPU都申请一个softnet_data数据结构,在这个数据结构里的poll_list是等待驱动程序将其poll函数注册进来,稍后网卡驱动初始化的时侯我们可以看见这一过程。
另外open_softirq注册了每一种软中断都注册一个处理函数。NET_TX_SOFTIRQ的处理函数为net_tx_action,NET_RX_SOFTIRQ的为net_rx_action。继续跟踪open_softirq后发觉这个注册的方法是记录在softirq_vec变量里的。前面ksoftirqd线程收到软中断的时侯,也会使用这个变量来找到每一种软中断对应的处理函数。
//file: kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *)){
softirq_vec[nr].action = action;
}
2.3合同栈注册
内核实现了网路层的ip合同,也实现了传输层的tcp合同和udp合同。那些合同对应的实现函数分别是ip_rcv(),tcp_v4_rcv()和udp_rcv()。和我们平常写代码的形式不一样的是,内核是通过注册的方法来实现的。Linux内核中的fs_initcall和subsys_initcall类似,也是初始化模块的入口。fs_initcall调用inet_init后开始网路合同栈注册。通过inet_init,将这种函数注册到了inet_protos和ptype_base数据结构中了。如右图:
相关代码如下
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,};static const struct net_protocol udp_protocol = {
.handler = udp_rcv,
.err_handler = udp_err,
.no_policy = 1,
.netns_ok = 1,};static const struct net_protocol tcp_protocol = {
.early_demux = tcp_v4_early_demux,
.handler = tcp_v4_rcv,
.err_handler = tcp_v4_err,
.no_policy = 1,
里面的代码中我们可以看见,udp_protocol结构体中的handler是udp_rcv,tcp_protocol结构体中的handler是tcp_v4_rcv,通过inet_add_protocol被初始化了进来。
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol){
if (!prot->netns_ok) {
pr_err("Protocol %u is not namespace aware, cannot register.n",
protocol);
return -EINVAL;
}
return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
inet_add_protocol函数将tcp和udp对应的处理函数都注册到了inet_protos链表中了。再看dev_add_pack(&ip_packet_type);这一行,ip_packet_type结构体中的type是合同名,func是ip_rcv函数,在dev_add_pack中会被注册到ptype_base哈希表中。
struct list_head *head = ptype_head(pt);
这儿我们须要记住inet_protos记录着udp,tcp的处理函数地址,ptype_base储存着ip_rcv()函数的处理地址。前面我们会听到软中断中会通过ptype_base找到ip_rcv函数地址,从而将ip包正确地送到ip_rcv()中执行。在ip_rcv上将会通过inet_protos找到tcp或则udp的处理函数,再而把包转发给udp_rcv()或tcp_v4_rcv()函数。
扩充一下,假如看一下ip_rcv和udp_rcv等函数的代码能看见好多合同的处理过程。诸如,ip_rcv中会处理netfilter和iptable过滤,假如你有好多或则很复杂的netfilter或iptables规则,这种规则都是在软中断的上下文中执行的,会加强网路延后。再比如,udp_rcv中会判定socket接收队列是否满了。对应的相关内核参数是net.core.rmem_max和net.core.rmem_default。假如有兴趣linux内核中的串口驱动嵌入式linux驱动程序设计从入门到精通,建议你们好好读一下inet_init这个函数的代码。
2.4网卡驱动初始化
每一个驱动程序(不仅仅只是网卡驱动)会使用module_init向内核注册一个初始化函数,当驱动被加载时,内核会调用这个函数。例如igb网卡驱动的代码坐落drivers/net/ethernet/intel/igb/igb_main.c
.name = igb_driver_name,
.id_table = igb_pci_tbl,
.probe = igb_probe,
.remove = igb_remove,
驱动的pci_register_driver调用完成后,Linux内核就晓得了该驱动的相关信息,例如igb网卡驱动的igb_driver_name和igb_probe函数地址等等。当网卡设备被辨识之后,内核会调用其驱动的probe方式(igb_driver的probe方式是igb_probe)。驱动probe方式执行的目的就是让设备ready,对于igb网卡,其igb_probe坐落drivers/net/ethernet/intel/igb/igb_main.c下。主要执行的操作如下:
第5步中我们看见,网卡驱动实现了ethtool所须要的插口,也在这儿注册完成函数地址的注册。当ethtool发起一个系统调用以后,内核会找到对应操作的反弹函数。对于igb网卡来说,其实现函数都在drivers/net/ethernet/intel/igb/igb_ethtool.c下。相信你此次能彻底理解ethtool的工作原理了吧?这个命令之所以能查看网卡收分包统计、能更改网卡自适应模式、能调整RX队列的数目和大小,是由于ethtool命令最终调用到了网卡驱动的相应方式,而不是ethtool本身有这个超能力。
第6步注册的igb_netdev_ops中包含的是igb_open等函数,该函数在网卡被启动的时侯会被调用。
.ndo_open = igb_open,
.ndo_stop = igb_close,
.ndo_start_xmit = igb_xmit_frame,
.ndo_get_stats64 = igb_get_stats64,
.ndo_set_rx_mode = igb_set_rx_mode,
.ndo_set_mac_address = igb_set_mac,
.ndo_change_mtu = igb_change_mtu,
第7步中,在igb_probe初始化过程中,还调用到了igb_alloc_q_vector。他注册了一个NAPI机制所必须的poll函数,对于igb网卡驱动来说,这个函数就是igb_poll,如下代码所示。
static int igb_alloc_q_vector(struct igb_adapter *adapter,
int v_count, int v_idx,
int txr_count, int txr_idx,
int rxr_count, int rxr_idx){
......
/* initialize NAPI */
netif_napi_add(adapter->netdev, &q_vector->napi,
2.5启动网卡
当里面的初始化都完成之后,就可以启动网卡了。追忆上面网卡驱动初始化时,我们谈到了驱动向内核注册了structurenet_device_ops变量,它包含着网卡启用、发包、设置mac地址等反弹函数(函数表针)。当启用一个网卡时(比如,通过ifconfigeth0up),net_device_ops中的igb_open方式会被调用。它一般会做以下事情:
/* allocate transmit descriptors */
err = igb_setup_all_tx_resources(adapter);
/* allocate receive descriptors */
err = igb_setup_all_rx_resources(adapter);
/* 注册中断处理函数 */
err = igb_request_irq(adapter);
if (err)
goto err_req_irq;
/* 启用NAPI */
for (i = 0; i num_q_vectors; i++)
napi_enable(&(adapter->q_vector[i]->napi));
在里面__igb_open函数调用了igb_setup_all_tx_resources,和igb_setup_all_rx_resources。在igb_setup_all_rx_resources这一步操作中,分配了RingBuffer,并完善显存和Rx队列的映射关系。(RxTx队列的数目和大小可以通过ethtool进行配置)。我们再接着看中断函数注册igb_request_irq:
static int igb_request_irq(struct igb_adapter *adapter){
if (adapter->msix_entries) {
err = igb_request_msix(adapter);
if (!err)
goto request_done;
......
在前面的代码中跟踪函数调用linux端口映射,__igb_open=>igb_request_irq=>igb_request_msix,在igb_request_msix中我们看见了,对于多队列的网卡,为每一个队列都注册了中断,其对应的中断处理函数是igb_msix_ring(该函数也在drivers/net/ethernet/intel/igb/igb_main.c下)。我们也可以见到,msix形式下,每位RX队列有独立的MSI-X中断,从网卡硬件中断的层面就可以设置让收到的包被不同的CPU处理。(可以通过irqbalance,或则更改/proc/irq/IRQ_NUMBER/smp_affinity就能更改和CPU的绑定行为)。
当作好以上打算工作之后,就可以开门迎客(数据包)了!