Linux TCP/IP协议栈之Socket的实现分析

时间:2022-07-20 11:04:42
数据包的接收

作者:kendo
http://www.skynet.org.cn/viewthread.php?tid=14&extra=page%3D1
Kernel:2.6.12

一、从网卡说起

这并非是一个网卡驱动分析的专门文档,只是对网卡处理数据包的流程进行一个重点的分析。这里以Intel的e100驱动为例进行分析。
大多数网卡都是一个PCI设备,PCI设备都包含了一个标准的配置寄存器,寄存器中,包含了PCI设备的厂商ID、设备ID等等信息,驱动
程序使用来描述这些寄存器的标识符。如下:

[Copy to clipboard][- ]CODE:struct pci_device_id {
        __u32 vendor, device;                /* Vendor and device ID or PCI_ANY_ID*/
        __u32 subvendor, subdevice;        /* Subsystem ID's or PCI_ANY_ID */
        __u32 class, class_mask;        /* (class,subclass,prog-if) triplet */
        kernel_ulong_t driver_data;        /* Data private to the driver */
};

这样,在驱动程序中,常常就可以看到定义一个struct pci_device_id 类型的数组,告诉内核支持不同类型的
PCI设备的列表,以e100驱动为例:

#define INTEL_8255X_ETHERNET_DEVICE(device_id, ich) {\
        PCI_VENDOR_ID_INTEL, device_id, PCI_ANY_ID, PCI_ANY_ID, \
        PCI_CLASS_NETWORK_ETHERNET << 8, 0xFFFF00, ich }
       
static struct pci_device_id e100_id_table[] = {
        INTEL_8255X_ETHERNET_DEVICE(0x1029, 0),
        INTEL_8255X_ETHERNET_DEVICE(0x1030, 0),
        INTEL_8255X_ETHERNET_DEVICE(0x1031, 3),
……/*略过一大堆支持的设备*/
        { 0, }
};

在内核中,一个PCI设备,使用struct pci_driver结构来描述,
struct pci_driver {
        struct list_head node;
        char *name;
        struct module *owner;
        const struct pci_device_id *id_table;        /* must be non-NULL for probe to be called */
        int  (*probe)  (struct pci_dev *dev, const struct pci_device_id *id);        /* New device inserted */
        void (*remove) (struct pci_dev *dev);        /* Device removed (NULL if not a hot-plug capable driver) */
        int  (*suspend) (struct pci_dev *dev, pm_message_t state);        /* Device suspended */
        int  (*resume) (struct pci_dev *dev);                        /* Device woken up */
        int  (*enable_wake) (struct pci_dev *dev, pci_power_t state, int enable);   /* Enable wake event */
        void (*shutdown) (struct pci_dev *dev);

        struct device_driver        driver;
        struct pci_dynids dynids;
};

因为在系统引导的时候,PCI设备已经被识别,当内核发现一个已经检测到的设备同驱动注册的id_table中的信息相匹配时,
它就会触发驱动的probe函数,以e100为例:
/*
* 定义一个名为e100_driver的PCI设备
* 1、设备的探测函数为e100_probe;
* 2、设备的id_table表为e100_id_table
*/
static struct pci_driver e100_driver = {
        .name =         DRV_NAME,
        .id_table =     e100_id_table,
        .probe =        e100_probe,
        .remove =       __devexit_p(e100_remove),
#ifdef CONFIG_PM
        .suspend =      e100_suspend,
        .resume =       e100_resume,
#endif

        .driver = {
                .shutdown = e100_shutdown,
        }

};

这样,如果系统检测到有与id_table中对应的设备时,就调用驱动的probe函数。

驱动设备在init函数中,调用pci_module_init函数初始化PCI设备e100_driver:

static int __init e100_init_module(void)
{
        if(((1 << debug) - 1) & NETIF_MSG_DRV) {
                printk(KERN_INFO PFX "%s, %s\n", DRV_DESCRIPTION, DRV_VERSION);
                printk(KERN_INFO PFX "%s\n", DRV_COPYRIGHT);
        }
        return pci_module_init(&e100_driver);
}

一切顺利的话,注册的e100_probe函数将被内核调用,这个函数完成两个重要的工作:
1、分配/初始化/注册网络设备;
2、完成PCI设备的I/O区域的分配和映射,以及完成硬件的其它初始化工作;

网络设备使用struct net_device结构来描述,这个结构非常之大,许多重要的参考书籍对它都有较为深入的描述,可以参考《Linux设备驱动程序》中网卡驱动设计的相关章节。我会在后面的内容中,对其重要的成员进行注释;

当probe函数被调用,证明已经发现了我们所支持的网卡,这样,就可以调用register_netdev函数向内核注册网络设备了,注册之前,一般会调用alloc_etherdev为以太网分析一个net_device,然后初始化它的重要成员。

除了向内核注册网络设备之外,探测函数另一项重要的工作就是需要对硬件进行初始化,比如,要访问其I/O区域,需要为I/O区域分配内存区域,然后进行映射,这一步一般的流程是:
1、request_mem_region()
2、ioremap()

对于一般的PCI设备而言,可以调用:
1、pci_request_regions()
2、ioremap()

pci_request_regions函数对PCI的6个寄存器都会调用资源分配函数进行申请(需要判断是I/O端口还是I/O内存),例如:

[Copy to clipboard][- ]CODE:int pci_request_regions(struct pci_dev *pdev, char *res_name)
{
        int i;
       
        for (i = 0; i < 6; i++)
                if(pci_request_region(pdev, i, res_name))
                        goto err_out;
        return 0;



[Copy to clipboard][- ]CODE:int pci_request_region(struct pci_dev *pdev, int bar, char *res_name)
{
        if (pci_resource_len(pdev, bar) == 0)
                return 0;
               
        if (pci_resource_flags(pdev, bar) & IORESOURCE_IO) {
                if (!request_region(pci_resource_start(pdev, bar),
                            pci_resource_len(pdev, bar), res_name))
                        goto err_out;
        }
        else if (pci_resource_flags(pdev, bar) & IORESOURCE_MEM) {
                if (!request_mem_region(pci_resource_start(pdev, bar),
                                        pci_resource_len(pdev, bar), res_name))
                        goto err_out;
        }
       
        return 0;

有了这些基础,我们来看设备的探测函数:
static int __devinit e100_probe(struct pci_dev *pdev,
        const struct pci_device_id *ent)
{
        struct net_device *netdev;
        struct nic *nic;
        int err;

        /*分配网络设备*/
        if(!(netdev = alloc_etherdev(sizeof(struct nic)))) {
                if(((1 << debug) - 1) & NETIF_MSG_PROBE)
                        printk(KERN_ERR PFX "Etherdev alloc failed, abort.\n");
                return -ENOMEM;
        }

        /*设置各成员指针函数*/
        netdev->open = e100_open;
        netdev->stop = e100_close;
        netdev->hard_start_xmit = e100_xmit_frame;
        netdev->get_stats = e100_get_stats;
        netdev->set_multicast_list = e100_set_multicast_list;
        netdev->set_mac_address = e100_set_mac_address;
        netdev->change_mtu = e100_change_mtu;
        netdev->do_ioctl = e100_do_ioctl;
        SET_ETHTOOL_OPS(netdev, &e100_ethtool_ops);
        netdev->tx_timeout = e100_tx_timeout;
        netdev->watchdog_timeo = E100_WATCHDOG_PERIOD;
        netdev->poll = e100_poll;
        netdev->weight = E100_NAPI_WEIGHT;
#ifdef CONFIG_NET_POLL_CONTROLLER
        netdev->poll_controller = e100_netpoll;
#endif
        /*设置网络设备名称*/
        strcpy(netdev->name, pci_name(pdev));

        /*取得设备私有数据结构*/
        nic = netdev_priv(netdev);
        /*网络设备指针,指向自己*/
        nic->netdev = netdev;
        /*PCIy设备指针,指向自己*/
        nic->pdev = pdev;
        nic->msg_enable = (1 << debug) - 1;
       
        /*将PCI设备的私有数据区指向网络设备*/
        pci_set_drvdata(pdev, netdev);

        /*激活PCI设备*/
        if((err = pci_enable_device(pdev))) {
                DPRINTK(PROBE, ERR, "Cannot enable PCI device, aborting.\n");
                goto err_out_free_dev;
        }

        /*判断I/O区域是否是I/O内存,如果不是,则报错退出*/
        if(!(pci_resource_flags(pdev, 0) & IORESOURCE_MEM)) {
                DPRINTK(PROBE, ERR, "Cannot find proper PCI device "
                        "base address, aborting.\n");
                err = -ENODEV;
                goto err_out_disable_pdev;
        }

        /*分配I/O内存区域*/
        if((err = pci_request_regions(pdev, DRV_NAME))) {
                DPRINTK(PROBE, ERR, "Cannot obtain PCI resources, aborting.\n");
                goto err_out_disable_pdev;
        }

        /*
         * 告之内核自己的DMA寻址能力,这里不是很明白,因为从0xFFFFFFFF来看,本来就是内核默认的32了
         * 为什么还要调用pci_set_dma_mask来重复设置呢?可能是对ULL而非UL不是很了解吧。
         */
        if((err = pci_set_dma_mask(pdev, 0xFFFFFFFFULL))) {
                DPRINTK(PROBE, ERR, "No usable DMA configuration, aborting.\n");
                goto err_out_free_res;
        }

        SET_MODULE_OWNER(netdev);
        SET_NETDEV_DEV(netdev, &pdev->dev);

        /*分配完成后,映射I/O内存*/
        nic->csr = ioremap(pci_resource_start(pdev, 0), sizeof(struct csr));
        if(!nic->csr) {
                DPRINTK(PROBE, ERR, "Cannot map device registers, aborting.\n");
                err = -ENOMEM;
                goto err_out_free_res;
        }

        if(ent->driver_data)
                nic->flags |= ich;
        else
                nic->flags &= ~ich;

        /*设置设备私有数据结构的大部份默认参数*/
        e100_get_defaults(nic);

        /* 初始化自旋锁,锅的初始化必须在调用 hw_reset 之前执行*/
        spin_lock_init(&nic->cb_lock);
        spin_lock_init(&nic->cmd_lock);

        /* 硬件复位,通过向指定I/O端口设置复位指令实现. */
        e100_hw_reset(nic);

        /*
         * PCI网卡被BIOS配置后,某些特性可能会被屏蔽掉。比如,多数BIOS都会清掉“master”位,
         * 这导致板卡不能随意向主存中拷贝数据。pci_set_master函数数会检查是否需要设置标志位,
         * 如果需要,则会将“master”位置位。
         * PS:什么是PCI master?
         * 不同于ISA总线,PCI总线的地址总线与数据总线是分时复用的。这样做的好处是,一方面
         * 可以节省接插件的管脚数,另一方面便于实现突发数据传输。在做数据传输时,由一个PCI
         * 设备做发起者(主控,Initiator或Master),而另一个PCI设备做目标(从设备,Target或Slave)。
         * 总线上的所有时序的产生与控制,都由Master来发起。PCI总线在同一时刻只能供一对设备完成传输。
         */
        pci_set_master(pdev);

        /*添加两个内核定时器,watchdog和blink_timer*/
        init_timer(&nic->watchdog);
        nic->watchdog.function = e100_watchdog;
        nic->watchdog.data = (unsigned long)nic;
        init_timer(&nic->blink_timer);
        nic->blink_timer.function = e100_blink_led;
        nic->blink_timer.data = (unsigned long)nic;

        INIT_WORK(&nic->tx_timeout_task,
                (void (*)(void *))e100_tx_timeout_task, netdev);

        if((err = e100_alloc(nic))) {
                DPRINTK(PROBE, ERR, "Cannot alloc driver memory, aborting.\n");
                goto err_out_iounmap;
        }

        /*phy寄存器初始化*/
        e100_phy_init(nic);

        if((err = e100_eeprom_load(nic)))
                goto err_out_free;

        memcpy(netdev->dev_addr, nic->eeprom, ETH_ALEN);
        if(!is_valid_ether_addr(netdev->dev_addr)) {
                DPRINTK(PROBE, ERR, "Invalid MAC address from "
                        "EEPROM, aborting.\n");
                err = -EAGAIN;
                goto err_out_free;
        }

        /* Wol magic packet can be enabled from eeprom */
        if((nic->mac >= mac_82558_D101_A4) &&
           (nic->eeprom[eeprom_id] & eeprom_id_wol))
                nic->flags |= wol_magic;

        /* ack any pending wake events, disable PME */
        pci_enable_wake(pdev, 0, 0);

        /*注册网络设备*/
        strcpy(netdev->name, "eth%d");
        if((err = register_netdev(netdev))) {
                DPRINTK(PROBE, ERR, "Cannot register net device, aborting.\n");
                goto err_out_free;
        }

        DPRINTK(PROBE, INFO, "addr 0x%lx, irq %d, "
                "MAC addr %02X:%02X:%02X:%02X:%02X:%02X\n",
                pci_resource_start(pdev, 0), pdev->irq,
                netdev->dev_addr[0], netdev->dev_addr[1], netdev->dev_addr[2],
                netdev->dev_addr[3], netdev->dev_addr[4], netdev->dev_addr[5]);

        return 0;

err_out_free:
        e100_free(nic);
err_out_iounmap:
        iounmap(nic->csr);
err_out_free_res:
        pci_release_regions(pdev);
err_out_disable_pdev:
        pci_disable_device(pdev);
err_out_free_dev:
        pci_set_drvdata(pdev, NULL);
        free_netdev(netdev);
        return err;
}
执行到这里,探测函数的使命就完成了,在对网络设备重要成员初始化时,有:
netdev->open = e100_open;
指定了设备的open函数为e100_open,这样,当第一次使用设备,比如使用ifconfig工具的时候,open函数将被调用。

二、打开设备

在探测函数中,设置了netdev->open = e100_open; 指定了设备的open函数为e100_open:

[Copy to clipboard][- ]CODE:static int e100_open(struct net_device *netdev)
{
        struct nic *nic = netdev_priv(netdev);
        int err = 0;

        netif_carrier_off(netdev);
        if((err = e100_up(nic)))
                DPRINTK(IFUP, ERR, "Cannot open interface, aborting.\n");
        return err;
}

大多数涉及物理设备可以感知信号载波(carrier)的存在,载波的存在意味着设备可以工作
据个例子来讲:当一个用户拔掉了网线,也就意味着信号载波的消失。
netif_carrier_off:关闭载波信号;
netif_carrier_on:打开载波信号;
netif_carrier_ok:检测载波信号;

对于探测网卡网线是否连接,这一组函数被使用得较多;

接着,调用e100_up函数启动网卡,这个“启动”的过程,最重要的步骤有:
1、调用request_irq向内核注册中断;
2、调用netif_wake_queue函数来重新启动传输队例;

[Copy to clipboard][- ]CODE:static int e100_up(struct nic *nic)
{
        int err;

        if((err = e100_rx_alloc_list(nic)))
                return err;
        if((err = e100_alloc_cbs(nic)))
                goto err_rx_clean_list;
        if((err = e100_hw_init(nic)))
                goto err_clean_cbs;
        e100_set_multicast_list(nic->netdev);
        e100_start_receiver(nic, 0);
        mod_timer(&nic->watchdog, jiffies);
        if((err = request_irq(nic->pdev->irq, e100_intr, SA_SHIRQ,
                nic->netdev->name, nic->netdev)))
                goto err_no_irq;
        netif_wake_queue(nic->netdev);
        netif_poll_enable(nic->netdev);
        /* enable ints _after_ enabling poll, preventing a race between
         * disable ints+schedule */
        e100_enable_irq(nic);
        return 0;

err_no_irq:
        del_timer_sync(&nic->watchdog);
err_clean_cbs:
        e100_clean_cbs(nic);
err_rx_clean_list:
        e100_rx_clean_list(nic);
        return err;

}

这样,中断函数e100_intr将被调用;

三、网卡中断

从本质上来讲,中断,是一种电信号,当设备有某种事件发生的时候,它就会产生中断,通过总线把电信号发送给中断控制器,如果中断的线是激活的,中断控制器 就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到内存中内核设置的中断处理程序的入口点,进行中断处理。
在内核中断处理中,会检测中断与我们刚才注册的中断号匹配,于是,注册的中断处理函数就被调用了。

当需要发/收数据,出现错误,连接状态变化等,网卡的中断信号会被触发。当接收到中断后,中断函数读取中断状态位,进行合法性判断,如判断中断信号是否是自己的等,然后,应答设备中断——OK,我已经知道了,你回去继续工作吧……
接着,它就屏蔽此中断,然后netif_rx_schedule函数接收,接收函数 会在未来某一时刻调用设备的poll函数(对这里而言,注册的是e100_poll)实现设备的轮询:

[Copy to clipboard][- ]CODE:static irqreturn_t e100_intr(int irq, void *dev_id, struct pt_regs *regs)
{
        struct net_device *netdev = dev_id;
        struct nic *nic = netdev_priv(netdev);
        u8 stat_ack = readb(&nic->csr->scb.stat_ack);

        DPRINTK(INTR, DEBUG, "stat_ack = 0x%02X\n", stat_ack);

        if(stat_ack == stat_ack_not_ours ||        /* Not our interrupt */
           stat_ack == stat_ack_not_present)        /* Hardware is ejected */
                return IRQ_NONE;

        /* Ack interrupt(s) */
        writeb(stat_ack, &nic->csr->scb.stat_ack);

        /* We hit Receive No Resource (RNR); restart RU after cleaning */
        if(stat_ack & stat_ack_rnr)
                nic->ru_running = RU_SUSPENDED;

        e100_disable_irq(nic);
        netif_rx_schedule(netdev);

        return IRQ_HANDLED;
}

对于数据包的接收而言,我们关注的是poll函数中,调用e100_rx_clean进行数据的接收:

[Copy to clipboard][- ]CODE:static int e100_poll(struct net_device *netdev, int *budget)
{
        struct nic *nic = netdev_priv(netdev);
       /*
         * netdev->quota是当前CPU能够从所有接口中接收数据包的最大数目,budget是在
         * 初始化阶段分配给接口的weight值,轮询函数必须接受二者之间的最小值。表示
         * 轮询函数本次要处理的数据包个数。
         */
        unsigned int work_to_do = min(netdev->quota, *budget);
        unsigned int work_done = 0;
        int tx_cleaned;

          /*进行数据包的接收和传输*/            
        e100_rx_clean(nic, &work_done, work_to_do);
        tx_cleaned = e100_tx_clean(nic);

         /*接收和传输完成后,就退出poll模块,重启中断*/
        /* If no Rx and Tx cleanup work was done, exit polling mode. */
        if((!tx_cleaned && (work_done == 0)) || !netif_running(netdev)) {
                netif_rx_complete(netdev);
                e100_enable_irq(nic);
                return 0;
        }

        *budget -= work_done;
        netdev->quota -= work_done;

        return 1;
}

static inline void e100_rx_clean(struct nic *nic, unsigned int *work_done,
        unsigned int work_to_do)
{
        struct rx *rx;
        int restart_required = 0;
        struct rx *rx_to_start = NULL;

        /* are we already rnr? then pay attention!!! this ensures that
         * the state machine progression never allows a start with a
         * partially cleaned list, avoiding a race between hardware
         * and rx_to_clean when in NAPI mode */
        if(RU_SUSPENDED == nic->ru_running)
                restart_required = 1;

        /* Indicate newly arrived packets */
        for(rx = nic->rx_to_clean; rx->skb; rx = nic->rx_to_clean = rx->next) {
                int err = e100_rx_indicate(nic, rx, work_done, work_to_do);
                if(-EAGAIN == err) {
                        /* hit quota so have more work to do, restart once
                         * cleanup is complete */
                        restart_required = 0;
                        break;
                } else if(-ENODATA == err)
                        break; /* No more to clean */
        }

        /* save our starting point as the place we'll restart the receiver */
        if(restart_required)
                rx_to_start = nic->rx_to_clean;

        /* Alloc new skbs to refill list */
        for(rx = nic->rx_to_use; !rx->skb; rx = nic->rx_to_use = rx->next) {
                if(unlikely(e100_rx_alloc_skb(nic, rx)))
                        break; /* Better luck next time (see watchdog) */
        }

        if(restart_required) {
                // ack the rnr?
                writeb(stat_ack_rnr, &nic->csr->scb.stat_ack);
                e100_start_receiver(nic, rx_to_start);
                if(work_done)
                        (*work_done)++;
        }
}

四、网卡的数据接收

内核如何从网卡接受数据,传统的经典过程:
1、数据到达网卡;
2、网卡产生一个中断给内核;
3、内核使用I/O指令,从网卡I/O区域中去读取数据;


我们在许多网卡驱动中,都可以在网卡的中断函数中见到这一过程。

但是,这一种方法,有一种重要的问题,就是大流量的数据来到,网卡会产生大量的中断,内核在中断上下文中,会浪费大量的资源来处理中断本身。所以,一个问 题是,“可不可以不使用中断”,这就是轮询技术,所谓NAPI技术,说来也不神秘,就是说,内核屏蔽中断,然后隔一会儿就去问网卡,“你有没有数据 啊?”……

从这个描述本身可以看到,哪果数据量少,轮询同样占用大量的不必要的CPU资源,大家各有所长吧,呵呵……

OK,另一个问题,就是从网卡的I/O区域,包括I/O寄存器或I/O内存中去读取数据,这都要CPU去读,也要占用CPU资源,“CPU从I/O区域 读,然后把它放到内存(这个内存指的是系统本身的物理内存,跟外设的内存不相干,也叫主内存)中”。于是自然地,就想到了DMA技术——让网卡直接从主内 存之间读写它们的I/O数据,CPU,这儿不干你事,自己找乐子去:
1、首先,内核在主内存中为收发数据建立一个环形的缓冲队列(通常叫DMA环形缓冲区)。
2、内核将这个缓冲区通过DMA映射,把这个队列交给网卡;
3、网卡收到数据,就直接放进这个环形缓冲区了——也就是直接放进主内存了;然后,向系统产生一个中断;
4、内核收到这个中断,就取消DMA映射,这样,内核就直接从主内存中读取数据;


——呵呵,这一个过程比传统的过程少了不少工作,因为设备直接把数据放进了主内存,不需要CPU的干预,效率是不是提高不少?

对应以上4步,来看它的具体实现:
1、分配环形DMA缓冲区
Linux内核中,用skb来描述一个缓存,所谓分配,就是建立一定数量的skb,然后把它们组织成一个双向链表;

2、建立DMA映射
内核通过调用
dma_map_single(struct device *dev,void *buffer,size_t size,enum dma_data_direction direction)
建立映射关系。
struct device *dev,描述一个设备;
buffer:把哪个地址映射给设备;也就是某一个skb——要映射全部,当然是做一个双向链表的循环即可;
size:缓存大小;
direction:映射方向——谁传给谁:一般来说,是“双向”映射,数据在设备和内存之间双向流动;

对于PCI设备而言(网卡一般是PCI的),通过另一个包裹函数pci_map_single,这样,就把buffer交给设备了!设备可以直接从里边读/取数据。

3、这一步由硬件完成;

4、取消映射
dma_unmap_single,对PCI而言,大多调用它的包裹函数pci_unmap_single,不取消的话,缓存控制权还在设备手里,要调用它,把主动权掌握在CPU手里——因为我们已经接收到数据了,应该由CPU把数据交给上层网络栈;

当然,不取消之前,通常要读一些状态位信息,诸如此类,一般是调用
dma_sync_single_for_cpu()
让CPU在取消映射前,就可以访问DMA缓冲区中的内容。

关于DMA映射的更多内容,可以参考《Linux设备驱动程序》“内存映射和DMA”章节相关内容!

OK,有了这些知识,我们就可以来看e100的代码了,它跟上面讲的步骤基本上一样的——绕了这么多圈子,就是想绕到e100上面了,呵呵!


在e100_open函数中,调用e100_up,我们前面分析它时,略过了一个重要的东东,就是环形缓冲区的建立,这一步,是通过
e100_rx_alloc_list函数调用完成的:

[Copy to clipboard][- ]CODE:static int e100_rx_alloc_list(struct nic *nic)
{
        struct rx *rx;
        unsigned int i, count = nic->params.rfds.count;

        nic->rx_to_use = nic->rx_to_clean = NULL;
        nic->ru_running = RU_UNINITIALIZED;

        /*结构struct rx用来描述一个缓冲区节点,这里分配了count个*/
        if(!(nic->rxs = kmalloc(sizeof(struct rx) * count, GFP_ATOMIC)))
                return -ENOMEM;
        memset(nic->rxs, 0, sizeof(struct rx) * count);

        /*虽然是连续分配的,不过还是遍历它,建立双向链表,然后为每一个rx的skb指针分员分配空间
        skb用来描述内核中的一个数据包,呵呵,说到重点了*/
        for(rx = nic->rxs, i = 0; i < count; rx++, i++) {
                rx->next = (i + 1 < count) ? rx + 1 : nic->rxs;
                rx->prev = (i == 0) ? nic->rxs + count - 1 : rx - 1;
                if(e100_rx_alloc_skb(nic, rx)) {                /*分配缓存*/
                        e100_rx_clean_list(nic);
                        return -ENOMEM;
                }
        }

        nic->rx_to_use = nic->rx_to_clean = nic->rxs;
        nic->ru_running = RU_SUSPENDED;

        return 0;
}



[Copy to clipboard][- ]CODE:#define RFD_BUF_LEN (sizeof(struct rfd) + VLAN_ETH_FRAME_LEN)
static inline int e100_rx_alloc_skb(struct nic *nic, struct rx *rx)
{
        /*skb缓存的分配,是通过调用系统函数dev_alloc_skb来完成的,它同内核栈中通常调用alloc_skb的区别在于,
        它是原子的,所以,通常在中断上下文中使用*/
        if(!(rx->skb = dev_alloc_skb(RFD_BUF_LEN + NET_IP_ALIGN)))
                return -ENOMEM;

        /*初始化必要的成员 */
        rx->skb->dev = nic->netdev;
        skb_reserve(rx->skb, NET_IP_ALIGN);
        /*这里在数据区之前,留了一块sizeof(struct rfd) 这么大的空间,该结构的
        一个重要作用,用来保存一些状态信息,比如,在接收数据之前,可以先通过
        它,来判断是否真有数据到达等,诸如此类*/
        memcpy(rx->skb->data, &nic->blank_rfd, sizeof(struct rfd));
        /*这是最关键的一步,建立DMA映射,把每一个缓冲区rx->skb->data都映射给了设备,缓存区节点
        rx利用dma_addr保存了每一次映射的地址,这个地址后面会被用到*/
        rx->dma_addr = pci_map_single(nic->pdev, rx->skb->data,
                RFD_BUF_LEN, PCI_DMA_BIDIRECTIONAL);

        if(pci_dma_mapping_error(rx->dma_addr)) {
                dev_kfree_skb_any(rx->skb);
                rx->skb = 0;
                rx->dma_addr = 0;
                return -ENOMEM;
        }

        /* Link the RFD to end of RFA by linking previous RFD to
         * this one, and clearing EL bit of previous.  */
        if(rx->prev->skb) {
                struct rfd *prev_rfd = (struct rfd *)rx->prev->skb->data;
                /*put_unaligned(val,ptr);用到把var放到ptr指针的地方,它能处理处理内存对齐的问题
                prev_rfd是在缓冲区开始处保存的一点空间,它的link成员,也保存了映射后的地址*/
                put_unaligned(cpu_to_le32(rx->dma_addr),
                        (u32 *)&prev_rfd->link);
                wmb();
                prev_rfd->command &= ~cpu_to_le16(cb_el);
                pci_dma_sync_single_for_device(nic->pdev, rx->prev->dma_addr,
                        sizeof(struct rfd), PCI_DMA_TODEVICE);
        }

        return 0;
}

e100_rx_alloc_list函数在一个循环中,建立了环形缓冲区,并调用e100_rx_alloc_skb为每个缓冲区分配了空间,并做了
DMA映射。这样,我们就可以来看接收数据的过程了。

前面我们讲过,中断函数中,调用netif_rx_schedule,表明使用轮询技术,系统会在未来某一时刻,调用设备的poll函数:

[Copy to clipboard][- ]CODE:static int e100_poll(struct net_device *netdev, int *budget)
{
        struct nic *nic = netdev_priv(netdev);
        unsigned int work_to_do = min(netdev->quota, *budget);
        unsigned int work_done = 0;
        int tx_cleaned;

        e100_rx_clean(nic, &work_done, work_to_do);
        tx_cleaned = e100_tx_clean(nic);

        /* If no Rx and Tx cleanup work was done, exit polling mode. */
        if((!tx_cleaned && (work_done == 0)) || !netif_running(netdev)) {
                netif_rx_complete(netdev);
                e100_enable_irq(nic);
                return 0;
        }

        *budget -= work_done;
        netdev->quota -= work_done;

        return 1;
}

目前,我们只关心rx,所以,e100_rx_clean函数就成了我们关注的对像,它用来从缓冲队列中接收全部数据(这或许是取名为clean的原因吧!):

[Copy to clipboard][- ]CODE:static inline void e100_rx_clean(struct nic *nic, unsigned int *work_done,
        unsigned int work_to_do)
{
        struct rx *rx;
        int restart_required = 0;
        struct rx *rx_to_start = NULL;

        /* are we already rnr? then pay attention!!! this ensures that
         * the state machine progression never allows a start with a
         * partially cleaned list, avoiding a race between hardware
         * and rx_to_clean when in NAPI mode */
        if(RU_SUSPENDED == nic->ru_running)
                restart_required = 1;

        /* 函数最重要的工作,就是遍历环形缓冲区,接收数据*/
        for(rx = nic->rx_to_clean; rx->skb; rx = nic->rx_to_clean = rx->next) {
                int err = e100_rx_indicate(nic, rx, work_done, work_to_do);
                if(-EAGAIN == err) {
                        /* hit quota so have more work to do, restart once
                         * cleanup is complete */
                        restart_required = 0;
                        break;
                } else if(-ENODATA == err)
                        break; /* No more to clean */
        }

        /* save our starting point as the place we'll restart the receiver */
        if(restart_required)
                rx_to_start = nic->rx_to_clean;

        /* Alloc new skbs to refill list */
        for(rx = nic->rx_to_use; !rx->skb; rx = nic->rx_to_use = rx->next) {
                if(unlikely(e100_rx_alloc_skb(nic, rx)))
                        break; /* Better luck next time (see watchdog) */
        }

        if(restart_required) {
                // ack the rnr?
                writeb(stat_ack_rnr, &nic->csr->scb.stat_ack);
                e100_start_receiver(nic, rx_to_start);
                if(work_done)
                        (*work_done)++;
        }
}



[Copy to clipboard][- ]CODE:static inline int e100_rx_indicate(struct nic *nic, struct rx *rx,
        unsigned int *work_done, unsigned int work_to_do)
{
        struct sk_buff *skb = rx->skb;
        struct rfd *rfd = (struct rfd *)skb->data;
        u16 rfd_status, actual_size;

        if(unlikely(work_done && *work_done >= work_to_do))
                return -EAGAIN;

        /* 读取数据之前,也就是取消DMA映射之前,需要先读取cb_complete 状态位,
        以确定数据是否真的准备好了,并且,rfd的actual_size中,也包含了真实的数据大小
        pci_dma_sync_single_for_cpu函数前面已经介绍过,它让CPU在取消DMA映射之前,具备
        访问DMA缓存的能力*/
        pci_dma_sync_single_for_cpu(nic->pdev, rx->dma_addr,
                sizeof(struct rfd), PCI_DMA_FROMDEVICE);
        rfd_status = le16_to_cpu(rfd->status);

        DPRINTK(RX_STATUS, DEBUG, "status=0x%04X\n", rfd_status);

        /* If data isn't ready, nothing to indicate */
        if(unlikely(!(rfd_status & cb_complete)))
                return -ENODATA;

        /* Get actual data size */
        actual_size = le16_to_cpu(rfd->actual_size) & 0x3FFF;
        if(unlikely(actual_size > RFD_BUF_LEN - sizeof(struct rfd)))
                actual_size = RFD_BUF_LEN - sizeof(struct rfd);

        /* 取消映射,因为通过DMA,网卡已经把数据放在了主内存中,这里一取消,也就意味着,
        CPU可以处理主内存中的数据了 */
        pci_unmap_single(nic->pdev, rx->dma_addr,
                RFD_BUF_LEN, PCI_DMA_FROMDEVICE);

        /* this allows for a fast restart without re-enabling interrupts */
        if(le16_to_cpu(rfd->command) & cb_el)
                nic->ru_running = RU_SUSPENDED;
       
        /*正确地设置data指针,因为最前面有一个sizeof(struct rfd)大小区域,跳过它*/
        skb_reserve(skb, sizeof(struct rfd));
        /*更新skb的tail和len指针,也是就更新接收到这么多数据的长度*/
        skb_put(skb, actual_size);
        /*设置协议位*/
        skb->protocol = eth_type_trans(skb, nic->netdev);

        if(unlikely(!(rfd_status & cb_ok))) {
                /* Don't indicate if hardware indicates errors */
                nic->net_stats.rx_dropped++;
                dev_kfree_skb_any(skb);
        } else if(actual_size > nic->netdev->mtu + VLAN_ETH_HLEN) {
                /* Don't indicate oversized frames */
                nic->rx_over_length_errors++;
                nic->net_stats.rx_dropped++;
                dev_kfree_skb_any(skb);
        } else {
                /*网卡驱动要做的最后一步,就是统计接收计数器,设置接收时间戳,然后调用netif_receive_skb,
                把数据包交给上层协议栈,自己的光荣始命也就完成了*/
                nic->net_stats.rx_packets++;
                nic->net_stats.rx_bytes += actual_size;
                nic->netdev->last_rx = jiffies;
                netif_receive_skb(skb);
                if(work_done)
                        (*work_done)++;
        }

        rx->skb = NULL;

        return 0;
}

网卡驱动执行到这里,数据接收的工作,也就处理完成了。但是,使用这一种方法的驱动,省去了网络栈中一个重要的内容,就是
“队列层”,让我们来看看,传统中断接收数据包模式下,使用netif_rx函数调用,又会发生什么。

五、队列层

1、软中断与下半部
当用中断处理的时候,为了减少中断处理的工作量,比如,一般中断处理时,需要屏蔽其它中断,如果中断处理时间过长,那么其它中断
有可能得不到及时处理,也以,有一种机制,就是把“不必马上处理”的工作,推迟一点,让它在中断处理后的某一个时刻得到处理。这就
是下半部。

下半部只是一个机制,它在Linux中,有多种实现方式,其中一种对时间要求最严格的实现方式,叫“软中断”,可以使用:

open_softirq()

来向内核注册一个软中断,
然后,在合适的时候,调用

raise_softirq_irqoff()

触发它。

如果采用中断方式接收数据(这一节就是在说中断方式接收,后面,就不用这种假设了),同样也需要软中断,可以调用

open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);

向内核注册一个名为NET_RX_SOFTIR的软中断,net_rx_action是软中断的处理函数。

然后,在驱动中断处理完后的某一个时刻,调用

raise_softirq_irqoff(NET_RX_SOFTIRQ);

触发它,这样net_rx_action将得到执行。

2、队列层
什么是队列层?通常,在网卡收发数据的时候,需要维护一个缓冲区队列,来缓存可能存在的突发数据,类似于前面的DMA环形缓冲区。
队列层中,包含了一个叫做struct softnet_data:

[Copy to clipboard][- ]CODE:struct softnet_data
{
        /*throttle 用于拥塞控制,当拥塞发生时,throttle将被设置,后续进入的数据包将被丢弃*/
        int                        throttle;
        /*netif_rx函数返回的拥塞级别*/
        int                        cng_level;
        int                        avg_blog;
        /*softnet_data 结构包含一个指向接收和传输队列的指针,input_pkt_queue成员指向准备传送
        给网络层的sk_buffs包链表的首部的指针,这个队列中的包是由netif_rx函数递交的*/
        struct sk_buff_head        input_pkt_queue;
       
        struct list_head        poll_list;
        struct net_device        *output_queue;
        struct sk_buff                *completion_queue;

        struct net_device        backlog_dev;        /* Sorry. 8) */
};

内核使用了一个同名的变量softnet_data,它是一个Per-CPU变量,每个CPU都有一个。

net/core/dev.c

[Copy to clipboard][- ]CODE:DECLARE_PER_CPU(struct softnet_data,softnet_data);


[Copy to clipboard][- ]CODE:/*
*       网络模块的核心处理模块.
*/
static int __init net_dev_init(void)
{
        int i, rc = -ENOMEM;

        BUG_ON(!dev_boot_phase);

        net_random_init();

        if (dev_proc_init())                /*初始化proc文件系统*/
                goto out;

        if (netdev_sysfs_init())        /*初始化sysfs文件系统*/
                goto out;

        /*ptype_all和ptype_base是重点,后面会详细分析,它们都是
        struct list_head类型变量,这里初始化链表成员*/
        INIT_LIST_HEAD(&ptype_all);
        for (i = 0; i < 16; i++)
                INIT_LIST_HEAD(&ptype_base[i]);

        for (i = 0; i < ARRAY_SIZE(dev_name_head); i++)
                INIT_HLIST_HEAD(&dev_name_head[i]);

        for (i = 0; i < ARRAY_SIZE(dev_index_head); i++)
                INIT_HLIST_HEAD(&dev_index_head[i]);

        /*
         *        初始化包接收队列,这里我们的重点了.
         */

        /*遍历每一个CPU,取得它的softnet_data,我们说过,它是一个struct softnet_data的Per-CPU变量*/
        for (i = 0; i < NR_CPUS; i++) {
                struct softnet_data *queue;
               
                /*取得第i个CPU的softnet_data,因为队列是包含在它里边的,所以,我会直接说,“取得队列”*/
                queue = &per_cpu(softnet_data, i);
                /*初始化队列头*/
                skb_queue_head_init(&queue->input_pkt_queue);
                queue->throttle = 0;
                queue->cng_level = 0;
                queue->avg_blog = 10; /* arbitrary non-zero */
                queue->completion_queue = NULL;
                INIT_LIST_HEAD(&queue->poll_list);
                set_bit(__LINK_STATE_START, &queue->backlog_dev.state);
                queue->backlog_dev.weight = weight_p;
                /*这里,队列中backlog_dev设备,它是一个伪网络设备,不对应任何物理设备,它的poll函数,指向了
                process_backlog,后面我们会详细分析*/
                queue->backlog_dev.poll = process_backlog;
                atomic_set(&queue->backlog_dev.refcnt, 1);
        }

#ifdef OFFLINE_SAMPLE
        samp_timer.expires = jiffies + (10 * HZ);
        add_timer(&samp_timer);
#endif

        dev_boot_phase = 0;
       
        /*注册收/发软中断*/
        open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
        open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);

        hotcpu_notifier(dev_cpu_callback, 0);
        dst_init();
        dev_mcast_init();
        rc = 0;
out:
        return rc;
}

这样,初始化完成后,在驱动程序中,在中断处理函数中,会调用netif_rx将数据交上来,这与采用轮询技术,有本质的不同:

[Copy to clipboard][- ]CODE:int netif_rx(struct sk_buff *skb)
{
        int this_cpu;
        struct softnet_data *queue;
        unsigned long flags;

        /* if netpoll wants it, pretend we never saw it */
        if (netpoll_rx(skb))
                return NET_RX_DROP;

        /*接收时间戳未设置,设置之*/
        if (!skb->stamp.tv_sec)
                net_timestamp(&skb->stamp);

        /*
         * 这里准备将数据包放入接收队列,需要禁止本地中断,在入队操作完成后,再打开中断.
         */
        local_irq_save(flags);
        /*获取当前CPU对应的softnet_data变量*/
        this_cpu = smp_processor_id();
        queue = &__get_cpu_var(softnet_data);

        /*接收计数器累加*/
        __get_cpu_var(netdev_rx_stat).total++;
       
        /*接收队列是否已满*/
        if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
                if (queue->input_pkt_queue.qlen) {
                        if (queue->throttle)                        /*拥塞发生了,丢弃数据包*/
                                goto drop;
                       
                        /*数据包入队操作*/
enqueue:
                        dev_hold(skb->dev);                        /*累加设备引入计数器*/
                        __skb_queue_tail(&queue->input_pkt_queue, skb);                /*将数据包加入接收队列*/
#ifndef OFFLINE_SAMPLE
                        get_sample_stats(this_cpu);
#endif
                        local_irq_restore(flags);
                        return queue->cng_level;
                }

                /*
                 * 驱动程序不断地调用net_rx函数,实现接收数据包的入队操作,当qlen == 0时, 则进入这段代码,这里,如果已经被设置拥塞标志的话,则清除它,因为这里将要调用软中断,开始将数据包交给 上层了,即上层协议的接收函数将执行出队操作,拥塞自然而然也就不存在了。 */
                if (queue->throttle)
                        queue->throttle = 0;

                /*
                 * netif_rx_schedule函数完成两件重要的工作:
                 * 1、将bakclog_dev设备加入“处理数据包的设备”的链表当中;
                 * 2、触发软中断函数,进行数据包接收处理;
                 */
                netif_rx_schedule(&queue->backlog_dev);
                goto enqueue;
        }

        /*前面判断了队列是否已满,如果已满而标志未设置,设置之,并累加拥塞计数器*/
        if (!queue->throttle) {
                queue->throttle = 1;
                __get_cpu_var(netdev_rx_stat).throttled++;
        }

/*拥塞发生,累加丢包计数器,释放数据包*/
drop:
        __get_cpu_var(netdev_rx_stat).dropped++;
        local_irq_restore(flags);

        kfree_skb(skb);
        return NET_RX_DROP;
}

从 这段代码的分析中,我们可以看到,当第一个数据包被接收后,因为qlen==0,所以首先会调用netif_rx_schedule触发软中断,然后利用 goto跳转至入队。因为软中断被触发后,将执行出队操作,把数据交往上层处理。而当这个时候,又有数据包进入,即网卡中断产生,因为它的优先级高过软中 断,这样,出队操作即被中断,网卡中断程序再将被调用,netif_rx函数又再次被执行,如果队列未满,就入队返回。中断完成后,软中断的执行过程被恢 复而继续执行出队——如此生产者/消费者循环不止,生生不息……

netif_rx调用netif_rx_schedule进一步处理数据包,我们注意到:
1、前面讨论过,采用轮询技术时,同样地,也是调用netif_rx_schedule,把设备自己传递了过去;
2、这里,采用中断方式,传递的是队列中的一个“伪设备”,并且,这个伪设备的poll函数指针,指向了一个叫做process_backlog的函数;

netif_rx_schedule函数完成两件重要的工作:
1、将bakclog_dev设备加入“处理数据包的设备”的链表当中;
2、触发软中断函数,进行数据包接收处理;

这样,我们可以猜想,在软中断函数中,不论是伪设备bakclog_dev,还是真实的设备(如前面讨论过的e100),都会被软中断函数以:
dev->poll()
的形式调用,对于e100来说,poll函数的接收过程已经分析了,而对于其它所有没有采用轮询技术的网络设备来说,它们将统统调用
process_backlog函数(我觉得把它改名为pseudo-poll是否更合适一些^o^)。

OK,我想分析到这里,关于中断处理与轮询技术的差异,已经基本分析开了……

继续来看,netif_rx_schedule进一步调用__netif_rx_schedule:

[Copy to clipboard][- ]CODE:/* Try to reschedule poll. Called by irq handler. */

static inline void netif_rx_schedule(struct net_device *dev)
{
        if (netif_rx_schedule_prep(dev))
                __netif_rx_schedule(dev);
}



[Copy to clipboard][- ]CODE:/* Add interface to tail of rx poll list. This assumes that _prep has
* already been called and returned 1.
*/

static inline void __netif_rx_schedule(struct net_device *dev)
{
        unsigned long flags;

        local_irq_save(flags);
        dev_hold(dev);
        /*伪设备也好,真实的设备也罢,都被加入了队列层的设备列表*/
        list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
        if (dev->quota < 0)
                dev->quota += dev->weight;
        else
                dev->quota = dev->weight;
        /*触发软中断*/
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);
        local_irq_restore(flags);
}

软中断被触发,注册的net_rx_action函数将被调用:

[Copy to clipboard][- ]CODE:/*接收的软中断处理函数*/
static void net_rx_action(struct softirq_action *h)
{
        struct softnet_data *queue = &__get_cpu_var(softnet_data);
        unsigned long start_time = jiffies;
        int budget = netdev_max_backlog;

       
        local_irq_disable();
       
        /*
         * 遍历队列的设备链表,如前所述,__netif_rx_schedule已经执行了
         * list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
         * 设备bakclog_dev已经被添加进来了
         */
        while (!list_empty(&queue->poll_list)) {
                struct net_device *dev;

                if (budget <= 0 || jiffies - start_time > 1)
                        goto softnet_break;

                local_irq_enable();
               
                /*取得链表中的设备*/
                dev = list_entry(queue->poll_list.next,
                                 struct net_device, poll_list);
                netpoll_poll_lock(dev);

                /*调用设备的poll函数,处理接收数据包,这样,采用轮询技术的网卡,它的真实的poll函数将被调用,
                这就回到我们上一节讨论的e100_poll函数去了,而对于采用传统中断处理的设备,它们调用的,都将是
                bakclog_dev的process_backlog函数*/
                if (dev->quota <= 0 || dev->poll(dev, &budget)) {
                        netpoll_poll_unlock(dev);
                       
                        /*处理完成后,把设备从设备链表中删除,又重置于末尾*/
                        local_irq_disable();
                        list_del(&dev->poll_list);
                        list_add_tail(&dev->poll_list, &queue->poll_list);
                        if (dev->quota < 0)
                                dev->quota += dev->weight;
                        else
                                dev->quota = dev->weight;
                } else {
                        netpoll_poll_unlock(dev);
                        dev_put(dev);
                        local_irq_disable();
                }
        }
out:
        local_irq_enable();
        return;

softnet_break:
        __get_cpu_var(netdev_rx_stat).time_squeeze++;
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);
        goto out;
}

对于dev->poll(dev, &budget)的调用,一个真实的poll函数的例子,我们已经分析过了,现在来看process_backlog,

[Copy to clipboard][- ]CODE:static int process_backlog(struct net_device *backlog_dev, int *budget)
{
        int work = 0;
        int quota = min(backlog_dev->quota, *budget);
        struct softnet_data *queue = &__get_cpu_var(softnet_data);
        unsigned long start_time = jiffies;

        backlog_dev->weight = weight_p;
       
        /*在这个循环中,执行出队操作,把数据从队列中取出来,交给netif_receive_skb,直至队列为空*/
        for (;;) {
                struct sk_buff *skb;
                struct net_device *dev;

                local_irq_disable();
                skb = __skb_dequeue(&queue->input_pkt_queue);
                if (!skb)
                        goto job_done;
                local_irq_enable();

                dev = skb->dev;

                netif_receive_skb(skb);

                dev_put(dev);

                work++;

                if (work >= quota || jiffies - start_time > 1)
                        break;

        }

        backlog_dev->quota -= work;
        *budget -= work;
        return -1;

/*当队列中的数据包被全部处理后,将执行到这里*/
job_done:
        backlog_dev->quota -= work;
        *budget -= work;

        list_del(&backlog_dev->poll_list);
        smp_mb__before_clear_bit();
        netif_poll_enable(backlog_dev);

        if (queue->throttle)
                queue->throttle = 0;
        local_irq_enable();
        return 0;
}

这个函数重要的工作,就是出队,然后调用netif_receive_skb()将数据包交给上层,这与上一节讨论的poll是一样的。这也是为什么,
在网卡驱动的编写中,采用中断技术,要调用netif_rx,而采用轮询技术,要调用netif_receive_skb啦!

到了这里,就处理完数据包与设备相关的部分了,数据包将进入上层协议栈……






1、什么是路由缓存
当数据包进入网络层后,需要做的第一件事情,就是进行路由查找,也即根据数据包的目标地址,查找需要转发的目的网关,当然,实际情况比这个要复杂,查找条件并不仅限于目的地址。

为了提高路由查找的效率,路由子系统引入了路由缓存的概念,用于某个目的地址在路由表中查找到匹配路由项后,就把目的地址对应的路由项缓存起来,可以通过route –C看到路由缓存表:

这样,下一次路由查找时,就先查缓存,没有在缓存中,再去搜索路由表。
不论进入或者是发出的数据,都要在网络层进行路由查找,查找首先在路由缓存中进行,如果命中,则查找完成,如果没有命中,则进入路由表中的查找,如果在表中查找命中,则创建相应的路由缓存项。

2、路由缓存表的组织
如前所述,整个缓存表由一条条地缓存项组成,整个表采用了hash表来组织,如下图。

从这张图中,我们可以看到许多重要的信息:

  • hash桶的大小由变量rt_hash_mask决定,它是决定路由缓存大小的重要因素;
  • 每一个hash桶的项,是由结构struct rt_hash_bucket;
  • 每一个路由缓存项,是由结构struct rtable结构描述的,它的rt_next/next成员来组织链表的;为什么是两个成员指针而不仅仅是一个next,这在后面来分析;
  • struct rtable缓存项中,还包含了一个struct dst_entry结构;


哈希桶的类型是rt_hash_bucket,该结构只包含指向缓存元素链表的一个指针和一个锁:

[Copy to clipboard][- ]CODE:struct rt_hash_bucket {
        struct rtable        *chain;
        spinlock_t        lock;
} __attribute__((__aligned__(8)));

chain成员指针自然指向下一个路由缓存项,用于链表的维护,另外,它还包含了一个自旋锁。关于锁的使用,这在后面代码分析中,可以看到。

Chain是一个struct rtable类型的指针,struct rtable用于描述一条完整的路由缓存项,它是路由子系统中,最重要的数据结构之一:

[Copy to clipboard][- ]CODE:struct rtable
{
        /*
         * 第一个成员u,被定义为一个联合体,这非常的重要,因为struct rtable被定义为每一项路由缓存,所以不可避免地在hash表中,存在冲撞的情况,rt_next指针就用来组织冲撞的链表。而另一方面, struct dst_entry结构的第一个成员struct dst_entry        *next;也指向了下一个冲撞节点struct rtable中的struct dst_entry,虽然这两个指针rt_next和dst.next的类型不同,但是它们却指向了同一内存位置(因为dst是struct rtable的第一个成员),这样,巧妙的设计,使得其很容易其享一些数据。一个指向rtable结构的指针可以安全地映射(typecast)为一个指 向dst_entry的指针,反之亦然。前面分析哈希表时,说同时用到rt_next和next两个成员指针来维护链表,就是这个道理。
         */       
        union
        {
                struct dst_entry        dst;
                struct rtable                *rt_next;
        } u;

        /*该指针指向egress设备的IP配置块。注意对送往本地的ingress报文的路由,设置的egress设备为loopback设备。*/
        struct in_device        *idev;
       
        unsigned                rt_flags;                /*路由标志*/
        __u16                        rt_type;                /*路由类型*/
        __u16                        rt_multipath_alg;        /*多路径缓存算法。该字段是根据相关路由项上配置的算法来初始化*/

        __u32                        rt_dst;                        /* 目的IP */
        __u32                        rt_src;                        /* 源IP */
        /*
Ingress设备标识。这个值是从ingress设备的net_device数据结构中得到。对本地生
成的流量(因此不是从任何接口上接收到的),该字段被设置为出设备的ifindex
字段。不要将该字段与本章后面描述的flowi数据结构fl中的iif字段混淆,对本地生
成的流量,fl中的iif字段被设置为零(loopback_dev)。
        */
        int                        rt_iif;

        /*
         * 当目的主机为直连时(即在同一链路上),rt_gateway表示目的地址。当需要通过一个网关到达目的地时,
         * rt_gateway被设置为由路由项中的下一跳网关。
         */
        __u32                        rt_gateway;

        /* 用于缓存查找的搜索key */
        struct flowi                fl;

        /* Miscellaneous cached information */
        __u32                        rt_spec_dst; /* RFC1122 specific destination */
        /*
inet_peer结构存储有关IP peer的long-living信息,IP peer是该缓存
路由项的目的IP地址对应的主机。与本地主机在最近一段时间通信的每个远端IP
地址都有一个inet_peer结构。       
        */       
        struct inet_peer        *peer; /* long-living peer info */
};

第 一个成员dst是struct dst_entry类型,它用于存储缓存路由项中独立于协议的信息,因为协议栈中除了IPV4,还有其它网络层协议,如IPV6,还要使用路由子系统。一 般是在网络层协议的缓存结构中,包含此结构,以用于存储私有信息,这里我们看到的在struct rtable中包含struct dst_entry就是一个典型的例子:

[Copy to clipboard][- ]CODE:struct dst_entry
{
        /*用于将分布在同一个哈希桶内的dst_entry实例链接在一起*/
        struct dst_entry        *next;
        /*引用计数*/
        atomic_t                __refcnt;        /* client references        */
        int                        __use;                /*该表项已经被使用的次数(即缓存查找返回该表项的次数)*/
        struct dst_entry        *child;
        struct net_device       *dev;                /*Egress设备(即将报文送达目的地的发送设备)*/
        /*
当fib_lookup API(只被IPv4使用)失败时,错误值被保存在error(用一个正值)
中,在后面的ip_error中使用该值来决定如何处理本次路由查找失败(即决定生成
哪一类ICMP消息)。       
        */
        short                        error;
        /*
         * 用于定义该dst_entry实例的可用状态:0(缺省值)表示该结构有效而且可以被使
用,2表示该结构将被删除因而不能被使用,-1被IPsec和IPv6使用但不被IPv4使
用。
         */
        short                        obsolete;
        /*
         * 标志集合(Set of flags)。DST_HOST被TCP使用,表示主机路由(即它不是到网
络或到一个广播/多播地址的路由)。DST_NOXFRM,DST_NOPOLICY和
DST_NOHASH只用于IPsec。
         */
        int                        flags;
#define DST_HOST                1
#define DST_NOXFRM                2
#define DST_NOPOLICY                4
#define DST_NOHASH                8
#define DST_BALANCED            0x10

/*
用于记录该表项上次被使用的时间戳。当缓存查找成功时更新该时间戳,垃圾回
收程序使用该时间戳来选择最合适的应当被释放的结构。
*/
        unsigned long                lastuse;
        unsigned long                expires;        /*表示该表项将过期的时间戳。*/

        unsigned short                header_len;        /* more space at head required */
        unsigned short                trailer_len;        /* space to reserve at tail */

        u32                        metrics[RTAX_MAX];
        struct dst_entry        *path;

        unsigned long                rate_last;        /* 这两个字段被用于对两种类型的ICMP消息限速。 */
        unsigned long                rate_tokens;

        struct neighbour        *neighbour;        /*neighbour是包含下一跳三层地址到二层地址映射的结构,hh是缓存的二层头。*/
        struct hh_cache                *hh;
        struct xfrm_state        *xfrm;

/*分别表示处理ingress报文和处理egress报文的函数。参见第33章“缓存查找”小节。*/
        int                        (*input)(struct sk_buff*);
        int                        (*output)(struct sk_buff*);

#ifdef CONFIG_NET_CLS_ROUTE
        __u32                        tclassid;        /*基于路由表的classifier的标签。*/
#endif

        struct  dst_ops                *ops;                /*该结构内的虚函数表(VFT)用于处理dst_entry结构。*/
        struct rcu_head                rcu_head;
               
        char                        info[0];
};

当然,结构是复杂的,可以暂时跳过其成员的含义,大约知道这个结构是拿来做什么的就可以了,我们在代码分析中,再回过头来分析其成员变量的含义。

2、缓存的初始化

明白了缓存hash表,现在来看这个表是如何被组织起来的:
net/ipv4/route.c

int __init ip_rt_init(void)
{
        ……
ipv4_dst_ops.kmem_cachep = kmem_cache_create("ip_dst_cache",
                                                     sizeof(struct rtable),
                                                     0, SLAB_HWCACHE_ALIGN,
                                                     NULL, NULL);

        if (!ipv4_dst_ops.kmem_cachep)
                panic("IP: failed to allocate ip_dst_cache\n");

        //计算出最大可需要的内存空间
        goal = num_physpages >> (26 - PAGE_SHIFT);
        if (rhash_entries)
                goal = (rhash_entries * sizeof(struct rt_hash_bucket)) >> PAGE_SHIFT;
        //该循环计算出此内存空间,需要的最大的order数
        for (order = 0; (1UL << order) < goal; order++)
                /* NOTHING */;

        do {
                //1UL << order,计算出分配的页面,再乘上页面大小,除以桶大小,计算出共可以有多少个hash桶
rt_hash_mask = (1UL << order) * PAGE_SIZE /
                        sizeof(struct rt_hash_bucket);
                while (rt_hash_mask & (rt_hash_mask - 1))
                        rt_hash_mask--;
                //分配hash表空间,它共有rt_hash_mask个桶
                rt_hash_table = (struct rt_hash_bucket *)
                        __get_free_pages(GFP_ATOMIC, order);
        } while (rt_hash_table == NULL && --order > 0);
        //上面这个while循环在分配失败后,一直尝试递减order,再尝试分配,直至分配分功或者order为0

        if (!rt_hash_table)
                panic("Failed to allocate IP route cache hash table\n");
……
}


初始化工作中,主要完成内存的计算,hash桶的空间的分配工作,这样,所有链表的链表首部就被建立起来了。整个hash表的框架就被建立起来了。当一个 缓存项需要被加入至这个表中,就根据相应的hash算法计算出hash值,然后使用rt_hash_table[hash]定位到链表的入口,利用前文所 述的struct rt_hash_bucket结构的chain成员组织链表,将其加入即可。因为我这里主要分析数据流的转发,重点是查找工作,缓存表的插入/删除/垃圾 回收等工作,就不在这里一一详细分析了。

3、缓存的查找

当数据包进入网络层后,第一个被调用的函数是ip_rcv函数:

[Copy to clipboard][- ]CODE:/* Main IP Receive routine. */
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt)
{
        struct iphdr *iph;

        /* 混杂模式下,数据将被丢弃 */
        if (skb->pkt_type == PACKET_OTHERHOST)
                goto drop;

        /*更新SNMP统计修筑*/
        IP_INC_STATS_BH(IPSTATS_MIB_INRECEIVES);

/*skb_share_check用于skb的共享检查,如果有别人已经在使用了,则克隆一份给自己使用*/
        if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
                IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
                goto out;
        }
        /*一个正确的IP包,包长度应该大于或等于包首部长度*/
        if (!pskb_may_pull(skb, sizeof(struct iphdr)))
                goto inhdr_error;

   /*取得IP首部*/
        iph = skb->nh.iph;

        /*
         *        RFC1122: 3.1.2.2 MUST silently discard any IP frame that fails the checksum.
         *
         *        Is the datagram acceptable?
         *
         *        1.        Length at least the size of an ip header
         *        2.        Version of 4
         *        3.        Checksums correctly. [Speed optimisation for later, skip loopback checksums]
         *        4.        Doesn't have a bogus length
         */
   /*长度和版本检查*/
        if (iph->ihl < 5 || iph->version != 4)
                goto inhdr_error;
       
        if (!pskb_may_pull(skb, iph->ihl*4))
                goto inhdr_error;
/*因为如果运行不好,上边pskb_may_pull函数会进一步去调用__pskb_pull_tail函数,去以完成补全数据包的页外数据的工作,把碎片部分
的数据线性重组,所以,有必要重置iph指针,以指向正确的ip 首部*/
        iph = skb->nh.iph;

    /*校验和检查*/
        if (ip_fast_csum((u8 *)iph, iph->ihl) != 0)
                goto inhdr_error;

        {
                __u32 len = ntohs(iph->tot_len);
                if (skb->len < len || len < (iph->ihl<<2))
                        goto inhdr_error;

                /* Our transport medium may have padded the buffer out. Now we know it
                 * is IP we can trim to the true length of the frame.
                 * Note this now means skb->len holds ntohs(iph->tot_len).
                 */
                if (pskb_trim_rcsum(skb, len)) {
                        IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
                        goto drop;
                }
        }
/*进入Netfilter钩子,处理完后,继续执行ip_rcv_finish */
        return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,
                       ip_rcv_finish);

inhdr_error:
        IP_INC_STATS_BH(IPSTATS_MIB_INHDRERRORS);
drop:
        kfree_skb(skb);
out:
        return NET_RX_DROP;
}

这一部份代码,简而言之,就是取得IP首部,进行合法性检查,然后调用ip_rcv_finish函数,关于Netfilter的更多内容,请参考九贱的《Linux防火墙设计与Nefilter源码分析》。

ip_rcv_finish 要做的第一件事情,就是调用ip_route_input函数进行缓存查找:

[Copy to clipboard][- ]CODE:static inline int ip_rcv_finish(struct sk_buff *skb)
{
        struct net_device *dev = skb->dev;
        struct iphdr *iph = skb->nh.iph;

        /*
         *        Initialise the virtual path cache for the packet. It describes
         *        how the packet travels inside Linux networking.
         */
        if (skb->dst == NULL) {
                if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))
                        goto drop;
        }
……

这就进入我们本章的主题了,接下来看看ip_route_input是如何进行缓存查找的。

[Copy to clipboard][- ]CODE:int ip_route_input(struct sk_buff *skb,                                        //数据包
  u32 daddr, u32 saddr,                                //目的地址和源地址
u8 tos,                                                         //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);

        rcu_read_lock();
        for (rth = rcu_dereference(rt_hash_table[hash].chain); rth;
             rth = rcu_dereference(rth->u.rt_next)) {
                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;
                        return 0;
                }
                RT_CACHE_STAT_INC(in_hlist_search);
        }
        rcu_read_unlock();

        if (MULTICAST(daddr)) {
                struct in_device *in_dev;

                rcu_read_lock();
                if ((in_dev = __in_dev_get(dev)) != NULL) {
                        int our = ip_check_mc(in_dev, daddr, saddr,
                                skb->nh.iph->protocol);
                        if (our
#ifdef CONFIG_IP_MROUTE
                            || (!LOCAL_MCAST(daddr) && IN_DEV_MFORWARD(in_dev))
#endif
                            ) {
                                rcu_read_unlock();
                                return ip_route_input_mc(skb, daddr, saddr,
                                                         tos, dev, our);
                        }
                }
                rcu_read_unlock();
                return -EINVAL;
        }
        return ip_route_input_slow(skb, daddr, saddr, tos, dev);
}

函数的第一个工作,就是根据目的地址、源地址、接口索引和TOS值计算hash值

[Copy to clipboard][- ]CODE:hash = rt_hash_code(daddr, saddr ^ (iif << 5), tos);
这里用到了rcu锁,关于这个锁的更多内容,可以参考其它相关资料。宏rcu_dereference在RCU读临界部份中取出一个RCU保护的指针。在需要内存屏障的体系中进行内存屏障:

[Copy to clipboard][- ]CODE:#define rcu_dereference(p)     ({ \
                                typeof(p) _________p1 = p; \
                                smp_read_barrier_depends(); \
                                (_________p1); \
                                })

于是,我们有了hash值后,就可以在hash桶中直接找到链表入口:

[Copy to clipboard][- ]CODE:struct rtable * rth;
rth = rcu_dereference(rt_hash_table[hash].chain);

如果要遍历该链表中的所有路由缓存项,就可以使用如下循环:

[Copy to clipboard][- ]CODE:        for (rth = rcu_dereference(rt_hash_table[hash].chain); rth;
             rth = rcu_dereference(rth->u.rt_next)) {
        ……
        }

遍历每一个缓存项就简单,重要的是如何将缓存中的路由特征同数据包的特征值进行匹配。
struct rtable中的fl成员,用于存储相关的路由特征值,也就是路由缓存查找匹配的关键字,它是一个struct flowi结构类型:

[Copy to clipboard][- ]CODE:struct flowi {
        /*Egress设备ID和ingress设备ID*/
        int        oif;
        int        iif;

        /*该联合的各个字段是可用于指定L3参数取值的结构。目前支持的协议为IPv4,IPv6和DECnet。*/
        union {
                struct {
                        __u32                        daddr;
                        __u32                        saddr;
                        __u32                        fwmark;
                        __u8                        tos;
                        __u8                        scope;
                } ip4_u;
               
                struct {
                        struct in6_addr                daddr;
                        struct in6_addr                saddr;
                        __u32                        flowlabel;
                } ip6_u;

                struct {
                        __u16                        daddr;
                        __u16                        saddr;
                        __u32                        fwmark;
                        __u8                        scope;
                } dn_u;
        } nl_u;
#define fld_dst                nl_u.dn_u.daddr
#define fld_src                nl_u.dn_u.saddr
#define fld_fwmark        nl_u.dn_u.fwmark
#define fld_scope        nl_u.dn_u.scope
#define fl6_dst                nl_u.ip6_u.daddr
#define fl6_src                nl_u.ip6_u.saddr
#define fl6_flowlabel        nl_u.ip6_u.flowlabel
#define fl4_dst                nl_u.ip4_u.daddr
#define fl4_src                nl_u.ip4_u.saddr
#define fl4_fwmark        nl_u.ip4_u.fwmark
#define fl4_tos                nl_u.ip4_u.tos
#define fl4_scope        nl_u.ip4_u.scope
       
        /*L4协议*/
        __u8        proto;
        /*该变量只定义了一个标志,FLOWI_FLAG_MULTIPATHOLDROUTE,它最初用于多路径代码,但已不再被使用。*/
        __u8        flags;
#define FLOWI_FLAG_MULTIPATHOLDROUTE 0x01

/*该联合的各个字段是可用于指定L4参数取值的主要结构。目前支持的协议为TCP,UDP,ICMP,DECnet和IPsec协议套件(suite)*/
        union {
                struct {
                        __u16        sport;
                        __u16        dport;
                } ports;

                struct {
                        __u8        type;
                        __u8        code;
                } icmpt;

                struct {
                        __u16        sport;
                        __u16        dport;
                        __u8        objnum;
                        __u8        objnamel; /* Not 16 bits since max val is 16 */
                        __u8        objname[16]; /* Not zero terminated */
                } dnports;

                __u32                spi;
        } uli_u;
#define fl_ip_sport        uli_u.ports.sport
#define fl_ip_dport        uli_u.ports.dport
#define fl_icmp_type        uli_u.icmpt.type
#define fl_icmp_code        uli_u.icmpt.code
#define fl_ipsec_spi        uli_u.spi
} __attribute__((__aligned__(BITS_PER_LONG/8)));

抛开其它协议和成员,联合体成员ip4_u就是IPV4协议关心的东东了:

[Copy to clipboard][- ]CODE:                struct {
                        __u32                        daddr;
                        __u32                        saddr;
                        __u32                        fwmark;
                        __u8                        tos;
                        __u8                        scope;
                } ip4_u;

于是,在遍历路由缓存项时,就可以使用如下语句来匹配路由缓存:

[Copy to clipboard][- ]CODE:                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)

分别对来源/目的地址,输入/输出设备,Netfilter防火墙的标记值和TOS值进行匹配。如果手气好,查找命中了:

[Copy to clipboard][- ]CODE:                        rth->u.dst.lastuse = jiffies;      //更新最近使用时间标记
                        dst_hold(&rth->u.dst);
                        rth->u.dst.__use++;                                //更新缓存使用记数器
                        RT_CACHE_STAT_INC(in_hit);
                        rcu_read_unlock();
                        [b]skb->dst = (struct dst_entry*)rth;        //设置skb的dst指针指向路由缓存项[/b]
                        return 0;

如果没有查到,怎么办?
当然,不是次次都能糊清一色的,如果没有命中的话,就要去查到路由表了。可以推想,网络栈在缓存查找没有命中后,会去搜索路由表,如果路由表匹配,会将由于产生的路由缓存项插入缓存表,以待下一次使用。关于路由表查找的相关内容,我会在下一章中分析。






一、sys_listen

对面向连接的协议,在调用bind(2)后,进一步调用listen(2),让套接字进入监听状态:

[Copy to clipboard][- ]CODE:int listen(int sockfd, int backlog);
backlog表示新建连接请求时,最大的未处理的积压请求数。

这里说到让套接字进入某种状态,也就是说,涉及到套接字的状态变迁,前面create和bind时,也遇到过相应的代码。

sock和sk都有相应的状态字段,先来看sock的:

[Copy to clipboard][- ]CODE:typedef enum {
        SS_FREE = 0,                        /* 套接字未分配                */
        SS_UNCONNECTED,                        /* 套接字未连接        */
        SS_CONNECTING,                        /* 套接字正在处理连接        */
        SS_CONNECTED,                        /* 套接字已连接                */
        SS_DISCONNECTING                /* 套接字正在处理关闭连接 */
} socket_state;

在创建套接字时,被初始化为SS_UNCONNECTED。

对于面向连接模式的SOCK_TREAM来讲,这样描述状态显然是不够的。这样,在sk中,使用sk_state维护了一个有限状态机来描述套接字的状态:

[Copy to clipboard][- ]CODE:enum {
  TCP_ESTABLISHED = 1,
  TCP_SYN_SENT,
  TCP_SYN_RECV,
  TCP_FIN_WAIT1,
  TCP_FIN_WAIT2,
  TCP_TIME_WAIT,
  TCP_CLOSE,
  TCP_CLOSE_WAIT,
  TCP_LAST_ACK,
  TCP_LISTEN,
  TCP_CLOSING,         /* now a valid state */

  TCP_MAX_STATES /* Leave at the end! */
};

还有一个相应的用来进行状态位运算的枚举结构:

[Copy to clipboard][- ]CODE:enum {
  TCPF_ESTABLISHED = (1 << 1),
  TCPF_SYN_SENT  = (1 << 2),
  TCPF_SYN_RECV  = (1 << 3),
  TCPF_FIN_WAIT1 = (1 << 4),
  TCPF_FIN_WAIT2 = (1 << 5),
  TCPF_TIME_WAIT = (1 << 6),
  TCPF_CLOSE     = (1 << 7),
  TCPF_CLOSE_WAIT = (1 << 8),
  TCPF_LAST_ACK  = (1 << 9),
  TCPF_LISTEN    = (1 << 10),
  TCPF_CLOSING   = (1 << 11)
};

值得一提的是,sk的状态不等于TCP的状态,虽然sk是面向协议栈,但它的状态并不能同TCP状态一一直接划等号。虽然这些状态值都用TCP-XXX来表式,但是只是因为TCP协议状态非常复杂。sk结构只是利用它的一个子集来抽像描述而已。

同样地,操作码SYS_LISTEN的任务会落到sys_listen()函数身上:

[Copy to clipboard][- ]CODE:/* Maximum queue length specifiable by listen.  */
#define SOMAXCONN        128
int sysctl_somaxconn = SOMAXCONN;

asmlinkage long sys_listen(int fd, int backlog)
{
        struct socket *sock;
        int err;
       
        if ((sock = sockfd_lookup(fd, &err)) != NULL) {
                if ((unsigned) backlog > sysctl_somaxconn)
                        backlog = sysctl_somaxconn;

                err = security_socket_listen(sock, backlog);
                if (err) {
                        sockfd_put(sock);
                        return err;
                }

                err=sock->ops->listen(sock, backlog);
                sockfd_put(sock);
        }
        return err;
}

同样地,函数会最终转向协议簇的listen函数,也就是inet_listen():

[Copy to clipboard][- ]CODE:/*
*        Move a socket into listening state.
*/
int inet_listen(struct socket *sock, int backlog)
{
        struct sock *sk = sock->sk;
        unsigned char old_state;
        int err;

        lock_sock(sk);

        err = -EINVAL;
        /* 在listen之前,sock必须为未连接状态,且只有 SOCK_STREAM 类型,才有listen(2)*/
        if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
                goto out;

        /* 临时保存状态机状态 */
        old_state = sk->sk_state;
        /* 只有状态机处于TCP_CLOSE 或者是TCP_LISTEN 这两种状态时,才可能对其调用listen(2) ,这个判断证明了listen(2)是可以重复调用地(当然是在转向TCP_LISTEN后没有再进行状态变迁*/
        if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
                goto out;

        /* 如果接口已经处理listen状态,只修改其max_backlog,否则先调用tcp_listen_start,继续设置协议的listen状态
         */
        if (old_state != TCP_LISTEN) {
                err = tcp_listen_start(sk);
                if (err)
                        goto out;
        }
        sk->sk_max_ack_backlog = backlog;
        err = 0;

out:
        release_sock(sk);
        return err;
}

inet_listen函数在确认sock->state和sk->sk_state状态后,会进一步调用tcp_listen_start函数,最且最后设置sk_max_ack_backlog 。

tcp的tcp_listen_start函数,完成两个重要的功能,一个是初始化sk的一些相关成员变量,另一方面是切换有限状态机的状态。 sk_max_ack_backlog表示监听时最大的backlog数量,它由用户空间传递的参数决定。而sk_ack_backlog表示当前的的 backlog数量。

当tcp服务器收到一个syn报文时,它表示了一个连接请求的到达。内核使用了一个hash表来维护这个连接请求表:
struct tcp_listen_opt
{
        u8                        max_qlen_log;        /* log_2 of maximal queued SYNs */
        int                        qlen;
        int                        qlen_young;
        int                        clock_hand;
        u32                        hash_rnd;
        struct open_request        *syn_table[TCP_SYNQ_HSIZE];
};

syn_table, 是open_request结构,就是连接请求表,连中的最大项,也就是最大允许的syn报文的数量,由max_qlen_log来决定。当套接字进入listen状态,也就是说可以接收syn报文了,那么在此之前,需要先初始化这个表:

[Copy to clipboard][- ]CODE:int tcp_listen_start(struct sock *sk)
{
        struct inet_sock *inet = inet_sk(sk);                //获取inet结构指针
        struct tcp_sock *tp = tcp_sk(sk);                //获取协议指针
        struct tcp_listen_opt *lopt;
       
        //初始化sk相关成员变量
        sk->sk_max_ack_backlog = 0;
        sk->sk_ack_backlog = 0;
       
        tp->accept_queue = tp->accept_queue_tail = NULL;
        rwlock_init(&tp->syn_wait_lock);
        tcp_delack_init(tp);
       
        //初始化连接请求hash表
        lopt = kmalloc(sizeof(struct tcp_listen_opt), GFP_KERNEL);
        if (!lopt)
                return -ENOMEM;

        memset(lopt, 0, sizeof(struct tcp_listen_opt));
        //初始化hash表容量,最小为6,其实际值由sysctl_max_syn_backlog决定
        for (lopt->max_qlen_log = 6; ; lopt->max_qlen_log++)
                if ((1 << lopt->max_qlen_log) >= sysctl_max_syn_backlog)
                        break;
        get_random_bytes(&lopt->hash_rnd, 4);

        write_lock_bh(&tp->syn_wait_lock);
        tp->listen_opt = lopt;
        write_unlock_bh(&tp->syn_wait_lock);

        /* There is race window here: we announce ourselves listening,
         * but this transition is still not validated by get_port().
         * It is OK, because this socket enters to hash table only
         * after validation is complete.
         */
         /* 修改状态机状态,表示进入listen状态,根据作者注释,当宣告自己进入listening状态后,但是这个状态转换并没有得到get_port的确 认。所以需要调用get_port()函数。但是对于一点,暂时还没有完全搞明白,只有留待后面再来分析它 */
        sk->sk_state = TCP_LISTEN;
        if (!sk->sk_prot->get_port(sk, inet->num)) {
                inet->sport = htons(inet->num);

                sk_dst_reset(sk);
                sk->sk_prot->hash(sk);

                return 0;
        }

        sk->sk_state = TCP_CLOSE;
        write_lock_bh(&tp->syn_wait_lock);
        tp->listen_opt = NULL;
        write_unlock_bh(&tp->syn_wait_lock);
        kfree(lopt);
        return -EADDRINUSE;
}

在切换了有限状态机状态后,调用了

[Copy to clipboard][- ]CODE:sk->sk_prot->hash(sk);
也就是tcp_v4_hash()函数。这里涉到到另一个hash表:TCP监听hash表。

二、TCP监听hash表
所谓TCP监听表,指的就内核维护“当前有哪些套接字在监听”的一个表,当一个数据包进入TCP栈的时候,内核查询这个表中对应的sk,以找到相应的数据 结构。(因为sk是面向网络栈调用的,找到了sk,就找到了tcp_sock,就找到了inet_sock,就找到了sock,就找到了fd……就到了组 织了)。

TCP所有的hash表都用了tcp_hashinfo来封装,前面分析bind已见过它:

[Copy to clipboard][- ]CODE:extern struct tcp_hashinfo {
                 ……
        /* All sockets in TCP_LISTEN state will be in here.  This is the only
         * table where wildcard'd TCP sockets can exist.  Hash function here
         * is just local port number.
         */
        struct hlist_head __tcp_listening_hash[TCP_LHTABLE_SIZE];

                 ……
        spinlock_t __tcp_portalloc_lock;
} tcp_hashinfo;

#define tcp_listening_hash (tcp_hashinfo.__tcp_listening_hash)

函数tcp_v4_hash将一个处理监听状态下的sk加入至这个hash表:

[Copy to clipboard][- ]CODE:static void tcp_v4_hash(struct sock *sk)
{
        if (sk->sk_state != TCP_CLOSE) {
                local_bh_disable();
                __tcp_v4_hash(sk, 1);
                local_bh_enable();
        }
}

因为__tcp_v4_hash不只用于监听hash表,它也用于其它hash表,其第二个参数listen_possible为真的时候,表示处理的是监听hash表:

[Copy to clipboard][- ]CODE:static __inline__ void __tcp_v4_hash(struct sock *sk, const int listen_possible)
{
        struct hlist_head *list;
        rwlock_t *lock;

        BUG_TRAP(sk_unhashed(sk));
        if (listen_possible && sk->sk_state == TCP_LISTEN) {
                list = &tcp_listening_hash[tcp_sk_listen_hashfn(sk)];
                lock = &tcp_lhash_lock;
                tcp_listen_wlock();
        } else {
                                ……
        }
        __sk_add_node(sk, list);
        sock_prot_inc_use(sk->sk_prot);
        write_unlock(lock);
        if (listen_possible && sk->sk_state == TCP_LISTEN)
                wake_up(&tcp_lhash_wait);
}

else中的部份用于另一个hash表,暂时不管它。代表很简单,如果确认是处理的是监听hash表。则先根据sk计算一个hash值,在hash桶中找到入口。再调用__sk_add_node加入至该hash链。

tcp_sk_listen_hashfn()函数事实上是tcp_lhashfn的包裹,前面已经分析过了。

__sk_add_node()函数也就是一个简单的内核hash处理函数hlist_add_head()的包裹:

[Copy to clipboard][- ]CODE:static __inline__ void __sk_add_node(struct sock *sk, struct hlist_head *list)
{
        hlist_add_head(&sk->sk_node, list);
}

小结

一个套接字的listen,主要需要做的工作有以下几件:
1、初始化sk相关的成员变量,最重要的是listen_opt,也就是连接请求hash表。
2、将sk的有限状态机转换为TCP_LISTEN,即监听状态;
3、将sk加入监听hash表;
4、设置允许的最大请求积压数,也就是sk的成员sk_max_ack_backlog的值。








第一部份 Socket套接字的创建

socket并不是TCP/IP协议的一部份。
从广义上来讲,socket是Unix/Linux抽像的进程间通讯的一种方法。网络socket通讯仅仅是其若干协议中的一类。而tcp/ip又是网络这类中的一种。
从tcp/ip的解度看socket,它更多地体现了用户API与协议栈的一个中间层接口层。用户通过调用socket API将报文递交给协议栈,或者从协议栈中接收报文件。

一、系统总入口

Linux内核为所有的与socket有关的操作的API,提供了一个统一的系统调用入口,其代码在net/socket.c中:

[Copy to clipboard][- ]CODE:asmlinkage long sys_socketcall(int call, unsigned long __user *args)
{
        unsigned long a[6];
        unsigned long a0,a1;
        int err;

        if(call<1||call>SYS_RECVMSG)
                return -EINVAL;

        /* copy_from_user should be SMP safe. */
        if (copy_from_user(a, args, nargs[call]))
                return -EFAULT;
               
        a0=a[0];
        a1=a[1];
       
        switch(call)
        {
                case SYS_SOCKET:
                        err = sys_socket(a0,a1,a[2]);
                        break;
                case SYS_BIND:
                        err = sys_bind(a0,(struct sockaddr __user *)a1, a[2]);
                        break;
                case SYS_CONNECT:
                        err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
                        break;
                case SYS_LISTEN:
                        err = sys_listen(a0,a1);
                        break;
                case SYS_ACCEPT:
                        err = sys_accept(a0,(struct sockaddr __user *)a1, (int __user *)a[2]);
                        break;
                case SYS_GETSOCKNAME:
                        err = sys_getsockname(a0,(struct sockaddr __user *)a1, (int __user *)a[2]);
                        break;
                case SYS_GETPEERNAME:
                        err = sys_getpeername(a0, (struct sockaddr __user *)a1, (int __user *)a[2]);
                        break;
                case SYS_SOCKETPAIR:
                        err = sys_socketpair(a0,a1, a[2], (int __user *)a[3]);
                        break;
                case SYS_SEND:
                        err = sys_send(a0, (void __user *)a1, a[2], a[3]);
                        break;
                case SYS_SENDTO:
                        err = sys_sendto(a0,(void __user *)a1, a[2], a[3],
                                         (struct sockaddr __user *)a[4], a[5]);
                        break;
                case SYS_RECV:
                        err = sys_recv(a0, (void __user *)a1, a[2], a[3]);
                        break;
                case SYS_RECVFROM:
                        err = sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
                                           (struct sockaddr __user *)a[4], (int __user *)a[5]);
                        break;
                case SYS_SHUTDOWN:
                        err = sys_shutdown(a0,a1);
                        break;
                case SYS_SETSOCKOPT:
                        err = sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]);
                        break;
                case SYS_GETSOCKOPT:
                        err = sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]);
                        break;
                case SYS_SENDMSG:
                        err = sys_sendmsg(a0, (struct msghdr __user *) a1, a[2]);
                        break;
                case SYS_RECVMSG:
                        err = sys_recvmsg(a0, (struct msghdr __user *) a1, a[2]);
                        break;
                default:
                        err = -EINVAL;
                        break;
        }
        return err;
}

首先调用copy_from_user将用户态参数拷贝至数组a。但是问题在于,每个被调用的API的参数不尽相同,那么每次拷贝的字节在小如果断定?
来看其第三个参数nargs[call],其中call是操作码,后面有个大大的switch...case就是判断它。对应的操作码定义在include/linux/net.h:

[Copy to clipboard][- ]CODE:#define SYS_SOCKET        1                /* sys_socket(2)                */
#define SYS_BIND        2                /* sys_bind(2)                        */
#define SYS_CONNECT        3                /* sys_connect(2)                */
#define SYS_LISTEN        4                /* sys_listen(2)                */
#define SYS_ACCEPT        5                /* sys_accept(2)                */
#define SYS_GETSOCKNAME        6                /* sys_getsockname(2)                */
#define SYS_GETPEERNAME        7                /* sys_getpeername(2)                */
#define SYS_SOCKETPAIR        8                /* sys_socketpair(2)                */
#define SYS_SEND        9                /* sys_send(2)                        */
#define SYS_RECV        10                /* sys_recv(2)                        */
#define SYS_SENDTO        11                /* sys_sendto(2)                */
#define SYS_RECVFROM        12                /* sys_recvfrom(2)                */
#define SYS_SHUTDOWN        13                /* sys_shutdown(2)                */
#define SYS_SETSOCKOPT        14                /* sys_setsockopt(2)                */
#define SYS_GETSOCKOPT        15                /* sys_getsockopt(2)                */
#define SYS_SENDMSG        16                /* sys_sendmsg(2)                */
#define SYS_RECVMSG        17                /* sys_recvmsg(2)                */

而数组nargs则根据操作码的不同,计算对应的参数的空间大小:

[Copy to clipboard][- ]CODE:/* Argument list sizes for sys_socketcall */
#define AL(x) ((x) * sizeof(unsigned long))
static unsigned char nargs[18]={AL(0),AL(3),AL(3),AL(3),AL(2),AL(3),
                                AL(3),AL(3),AL(4),AL(4),AL(4),AL(6),
                                AL(6),AL(2),AL(5),AL(5),AL(3),AL(3)};
#undef AL

当拷贝完成参数后,就进入一个switch...case...判断操作码,跳转至对应的系统接口。

二、 sys_socket函数

操作码SYS_SOCKET是由sys_socket()实现的:

[Copy to clipboard][- ]CODE:asmlinkage long sys_socket(int family, int type, int protocol)
{
        int retval;
        struct socket *sock;

        retval = sock_create(family, type, protocol, &sock);
        if (retval < 0)
                goto out;

        retval = sock_map_fd(sock);
        if (retval < 0)
                goto out_release;

out:
        /* It may be already another descriptor 8) Not kernel problem. */
        return retval;

out_release:
        sock_release(sock);
        return retval;
}

在分析这段代码之间,首先来看,创建一个Socket,对内核而言,究竟意味着什么?究竟需要内核干什么事?

当用户空间要创建一个socke接口时,会调用API函数:

[Copy to clipboard][- ]CODE:int socket(int domain, int type, int protocol);
函数,其三个参数分别表示协议族、协议类型(面向连接或无连接)以及协议。

对于用户态而言,一个Scoket,就是一个特殊的,已经打开的文件。为了对socket抽像出文件的概念,内核中为socket定义了一个专门的文件系统类型sockfs:
static struct vfsmount *sock_mnt;

[Copy to clipboard][- ]CODE:static struct file_system_type sock_fs_type = {
        .name =                "sockfs",
        .get_sb =        sockfs_get_sb,
        .kill_sb =        kill_anon_super,
};

在模块初始化的时候,安装该文件系统:

[Copy to clipboard][- ]CODE:void __init sock_init(void)
{
        ……
        register_filesystem(&sock_fs_type);
        sock_mnt = kern_mount(&sock_fs_type);       
}

稍后还要回来继续分析安装中的一点细节。

有了文件系统后,对内核而言,创建一个socket,就是在sockfs文件系统中创建一个文件节点(inode),并建立起为了实现socket功能所 需的一整套数据结构,包括struct inode和struct socket结构。struct socket结构在内核中,就代表了一个"Socket",当一个struct socket数据结构被分配空间后,再将其与一个已打开的文件“建立映射关系”。这样,用户态就可以用抽像的文件的概念来操作socket了——当然,由 于网络的特殊性,至少就目前而言,这种抽像,并不如其它模块的抽像那么完美。

这里socket的实现,和文件系统密切相关。这里就不再分析Linux的文件系统了,这里只分配与socket相关的一些细节,其它的都一一跳过,呵呵,希望也能有水平再写一篇《Linux文件系统的设计与实现简析》。

文件系统struct vfsmount中有一个成员指针mnt_sb指向该文件系统的超级块,而超级块结构struct super_lock有一个重要的成员s_op指向了超级块的操作函数表,其中有函数指针alloc_inode()即为在给定的超级块下创建并初始化一 个新的索引节点对像。也就是调用:

[Copy to clipboard][- ]CODE:sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb);
当然,连同相关的处理细节一起,这一操作被层层封装至一个上层函数 new_inode()。

那如何分配一个struct socket结构呢?如前所述,一个socket总是与一个inode密切相关的。当然,在inode中,设置一个socket成员,是完全可行的,但是 这貌似浪费了空间——毕竟,更多的文件系统没有socket这个东东。所以,内核引入了另一个socket_alloc结构:

[Copy to clipboard][- ]CODE:struct socket_alloc {
        struct socket socket;
        struct inode vfs_inode;
};

显而易见,该结构实现了inode和socket的封装。已经一个inode,可以通过宏SOCKET_I来获取与之对应的socket:
sock = SOCKET_I(inode);

static inline struct socket *SOCKET_I(struct inode *inode)
{
        return &container_of(inode, struct socket_alloc, vfs_inode)->socket;
}

但是,这样做,也同时意味着,在分配一个inode后,必须再分配一个socket_alloc结构,并实现对应的封装。否则,container_of又能到哪儿去找到socket呢?现在来简要地看一个这个流程——这是文件系统安装中的一个重要步骤:

[Copy to clipboard][- ]CODE:struct vfsmount *kern_mount(struct file_system_type *type)
{
        return do_kern_mount(type->name, 0, type->name, NULL);
}



[Copy to clipboard][- ]CODE:struct vfsmount *
do_kern_mount(const char *fstype, int flags, const char *name, void *data)
{
        struct file_system_type *type = get_fs_type(fstype);
        struct super_block *sb = ERR_PTR(-ENOMEM);
                ……
                sb = type->get_sb(type, flags, name, data);
                ……
               mnt->mnt_sb = sb;
               ……
}

do_kern_mount函数中,先根据注册的文件系统类型,调用get_fs_type获取之,也就是我们之前注册的sock_fs_type,然后调用它的get_sb成员函数指针,获取相应的超级块sb。最后,调置文件系统的超级块成员指针,使之指向对应的值。
这里get_sb函数指针,指向之前初始化的sockfs_get_sb()函数。
static struct super_block *sockfs_get_sb(struct file_system_type *fs_type,
        int flags, const char *dev_name, void *data)
{
        return get_sb_pseudo(fs_type, "socket:", &sockfs_ops, SOCKFS_MAGIC);
}

注意其第三个参数sockfs_ops,它封装了sockfs的功能函数表:

[Copy to clipboard][- ]CODE:static struct super_operations sockfs_ops = {
        .alloc_inode =        sock_alloc_inode,
        .destroy_inode =sock_destroy_inode,
        .statfs =        simple_statfs,
};



[Copy to clipboard][- ]CODE:struct super_block *
get_sb_pseudo(struct file_system_type *fs_type, char *name,
        struct super_operations *ops, unsigned long magic)
{
        struct super_block *s = sget(fs_type, NULL, set_anon_super, NULL);
                ……
       
                s->s_op = ops ? ops : &default_ops;
}

这里就是先获取/分配一个超级块,然后初始化超级块的各成员,包括s_op,我们前面提到过它,它封装了对应的功能函数表。这里s_op自然就指向了sockfs_ops。那前面提到的new_inode()函数分配inode时调用的:

[Copy to clipboard][- ]CODE:sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb);
这个alloc_inode函数指针也就是sockfs_ops的sock_alloc_inode()函数——转了一大圈,终于指到它了。
来看看sock_alloc_inode是如何分配一个inode节点的:

[Copy to clipboard][- ]CODE:static struct inode *sock_alloc_inode(struct super_block *sb)
{
        struct socket_alloc *ei;
        ei = (struct socket_alloc *)kmem_cache_alloc(sock_inode_cachep, SLAB_KERNEL);
        if (!ei)
                return NULL;
        init_waitqueue_head(&ei->socket.wait);
       
        ei->socket.fasync_list = NULL;
        ei->socket.state = SS_UNCONNECTED;
        ei->socket.flags = 0;
        ei->socket.ops = NULL;
        ei->socket.sk = NULL;
        ei->socket.file = NULL;
        ei->socket.flags = 0;

        return &ei->vfs_inode;
}

函数先分配了一个用于封装socket和inode的ei,然后在高速缓存中为之申请了一块空间。这样,inode和socket就同时都被分配了。接下来初始化socket的各个成员,这些成员,在后面都会一一提到。

[Copy to clipboard][- ]CODE:/**
*  struct socket - general BSD socket
*  @state: socket state (%SS_CONNECTED, etc)
*  @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc)
*  @ops: protocol specific socket operations
*  @fasync_list: Asynchronous wake up list
*  @file: File back pointer for gc
*  @sk: internal networking protocol agnostic socket representation
*  @wait: wait queue for several uses
*  @type: socket type (%SOCK_STREAM, etc)
*/
struct socket {
        socket_state                state;
        unsigned long                flags;
        struct proto_ops        *ops;
        struct fasync_struct        *fasync_list;
        struct file                *file;
        struct sock                *sk;
        wait_queue_head_t        wait;
        short                        type;
};

OK,至目前为止,分配inode、socket以及两者如何关联,都已一一分析了。
最后一个关键问题,就是如何把socket与一个已打开的文件,建立映射关系。

在内核中,用struct file结构描述一个已经打开的文件,指向该结构的指针内核中通常用file或filp来描述。我们知道,内核中,可以通过全局项current来获得当 前进程,它是一个struct task_struct类型的指针。tastk_struct有一个成员:
struct files_struct *files;
指向一个已打开的文件。当然,由于一个进程可能打开多个文件,所以,struct files_struct结构有
struct file * fd_array[NR_OPEN_DEFAULT];
成员,这是个数组,以文件描述符为下标,即current->files->fd[fd],可以找到与当前进程指定文件描述符的文件。

有了这些基础,如果要把一个socket与一个已打开的文件建立映射,首先要做的就是为socket分配一个struct file,并申请分配一个相应的文件描述符fd。因为socket并不支持open方法(前面说socket的文件界面的抽像并不完美,这应该是一个佐证 吧?),所以不能期望用户界面通过调用open() API来分配一个struct file,而是通过调用get_empty_filp来获取:

[Copy to clipboard][- ]CODE:struct file *file = get_empty_filp();
同样地:

[Copy to clipboard][- ]CODE:int fd;
fd = get_unused_fd();

获取一个空间的文件描述符

然后,让current的files指针的fd数组的fd索引项指向该file:

[Copy to clipboard][- ]CODE:void fastcall fd_install(unsigned int fd, struct file * file)
{
        struct files_struct *files = current->files;
        spin_lock(&files->file_lock);
        if (unlikely(files->fd[fd] != NULL))
                BUG();
        files->fd[fd] = file;
        spin_unlock(&files->file_lock);
}

OK, 做到这一步,有了一个文件描述符fd和一个打开的文件file,它们与当前进程相连,但是好像与创建的socket并无任何瓜葛。要做的映射还是没有进 展。struct file或者文件描述述fd或current都没有任何能够与inode或者是socket相关的东东。这需要一个中间的桥梁,目录项:struct dentry结构。
因为一个文件都有与其对应的目录项:
struct file {
        struct list_head        f_list;
        struct dentry                *f_dentry;
……

而一个目录项:
struct dentry {
……
        struct inode *d_inode;                /* Where the name belongs to - NULL is
                                         * negative */

d_inode成员指向了与之对应的inode节点……

而之前已经创建了一个inode节点和与之对应的socket。
所以,现在要做的,就是:
“先为当前文件分配一个对应的目录项,再将已创建的inode节点安装至该目录项”
这样,一个完成的映射关系:
进程、文件描述符、打开文件、目录项、inode节点、socket就完整地串起来了。

基本要分析的一些前导的东东都一一罗列了,虽然已尽量避免陷入文件系统的细节分析,但是还是不可避免地进入其中,因为它们关系实现太紧密了。现在可以来看套接字的创建过程了:

[Copy to clipboard][- ]CODE:asmlinkage long sys_socket(int family, int type, int protocol)
{
        int retval;
        struct socket *sock;

        retval = sock_create(family, type, protocol, &sock);
        if (retval < 0)
                goto out;

        retval = sock_map_fd(sock);
        if (retval < 0)
                goto out_release;

out:
        /* It may be already another descriptor 8) Not kernel problem. */
        return retval;

out_release:
        sock_release(sock);
        return retval;
}



[Copy to clipboard][- ]CODE:int sock_create(int family, int type, int protocol, struct socket **res)
{
        return __sock_create(family, type, protocol, res, 0);
}

三、af_inet协议簇的协议封装

接下来,函数调用之前已经注的inet_family_ops的函数指针create,也就是inet_create()函数,前面,可以说一个通用的 socket已经创建好了,这里要完成与协议本身相关的一些创建socket的工作。这一部份的工作比较复杂,还是先来看看af_inet.c中的模块初 始化时候,做了哪些与此相关的工作。

要引入的第一个数据结构是struct inet_protosw,它封装了一个协议类型(如SOCK_STREAM、SOCK_DGRAM等)与ip协议中对应的传输层协议:

[Copy to clipboard][- ]CODE:/* This is used to register socket interfaces for IP protocols.  */
struct inet_protosw {
        struct list_head list;

        /* These two fields form the lookup key.  */
        unsigned short         type;           /* This is the 2nd argument to socket(2). */
        int                 protocol; /* This is the L4 protocol number.  */

        struct proto         *prot;
        struct proto_ops *ops;
  
        int              capability; /* Which (if any) capability do
                                      * we need to use this socket
                                      * interface?
                                      */
        char             no_check;   /* checksum on rcv/xmit/none? */
        unsigned char         flags;      /* See INET_PROTOSW_* below.  */
};
#define INET_PROTOSW_REUSE 0x01             /* Are ports automatically reusable? */
#define INET_PROTOSW_PERMANENT 0x02  /* Permanent protocols are unremovable. */

type 是协议类型,对于ipv4而言,就是SOCK_STREAM、SOCK_DGRAM或者是SOCK_RAW之一。protocol是传输层的协议号。 prot用于描述一个具体的传输层协议,而ops指向对应的当前协议类型的操作函数集。针对不同的协议类型,定义了不同的ops:

[Copy to clipboard][- ]CODE:struct proto_ops inet_stream_ops = {
        .family =        PF_INET,
        .owner =        THIS_MODULE,
        .release =        inet_release,
        .bind =                inet_bind,
        .connect =        inet_stream_connect,
        .socketpair =        sock_no_socketpair,
        .accept =        inet_accept,
        .getname =        inet_getname,
        .poll =                tcp_poll,
        .ioctl =        inet_ioctl,
        .listen =        inet_listen,
        .shutdown =        inet_shutdown,
        .setsockopt =        sock_common_setsockopt,
        .getsockopt =        sock_common_getsockopt,
        .sendmsg =        inet_sendmsg,
        .recvmsg =        sock_common_recvmsg,
        .mmap =                sock_no_mmap,
        .sendpage =        tcp_sendpage
};



[Copy to clipboard][- ]CODE:struct proto_ops inet_dgram_ops = {
        .family =        PF_INET,
        .owner =        THIS_MODULE,
        .release =        inet_release,
        .bind =                inet_bind,
        .connect =        inet_dgram_connect,
        .socketpair =        sock_no_socketpair,
        .accept =        sock_no_accept,
        .getname =        inet_getname,
        .poll =                udp_poll,
        .ioctl =        inet_ioctl,
        .listen =        sock_no_listen,
        .shutdown =        inet_shutdown,
        .setsockopt =        sock_common_setsockopt,
        .getsockopt =        sock_common_getsockopt,
        .sendmsg =        inet_sendmsg,
        .recvmsg =        sock_common_recvmsg,
        .mmap =                sock_no_mmap,
        .sendpage =        inet_sendpage,
};



[Copy to clipboard][- ]CODE:/*
* For SOCK_RAW sockets; should be the same as inet_dgram_ops but without
* udp_poll
*/
static struct proto_ops inet_sockraw_ops = {
        .family =        PF_INET,
        .owner =        THIS_MODULE,
        .release =        inet_release,
        .bind =                inet_bind,
        .connect =        inet_dgram_connect,
        .socketpair =        sock_no_socketpair,
        .accept =        sock_no_accept,
        .getname =        inet_getname,
        .poll =                datagram_poll,
        .ioctl =        inet_ioctl,
        .listen =        sock_no_listen,
        .shutdown =        inet_shutdown,
        .setsockopt =        sock_common_setsockopt,
        .getsockopt =        sock_common_getsockopt,
        .sendmsg =        inet_sendmsg,
        .recvmsg =        sock_common_recvmsg,
        .mmap =                sock_no_mmap,
        .sendpage =        inet_sendpage,
};

从各个函数指针的名称,我们就可以大约知道它们是做什么事的了。进一步进以看到,它们的函数指针指向的函数差不多都是相同的。除了一些细节上的区别,例如后面两种协议类型并不支持listen。

socket() API第二个参数是协议类型,第三个参数是该协议类型下的协议——不过对于ipv4而言,它们都是一一对应的。但是从抽像封装的角度看,数据结构的设计本 身应该满足一个协议类型下边,可能存在多个不同的协议,即一对多的情况。而一一对应,仅是它们的特例:

[Copy to clipboard][- ]CODE:/* Upon startup we insert all the elements in inetsw_array[] into
* the linked list inetsw.
*/
static struct inet_protosw inetsw_array[] =
{
        {
                .type =       SOCK_STREAM,
                .protocol =   IPPROTO_TCP,
                .prot =       &tcp_prot,
                .ops =        &inet_stream_ops,
                .capability = -1,
                .no_check =   0,
                .flags =      INET_PROTOSW_PERMANENT,
        },

        {
                .type =       SOCK_DGRAM,
                .protocol =   IPPROTO_UDP,
                .prot =       &udp_prot,
                .ops =        &inet_dgram_ops,
                .capability = -1,
                .no_check =   UDP_CSUM_DEFAULT,
                .flags =      INET_PROTOSW_PERMANENT,
       },
        

       {
               .type =       SOCK_RAW,
               .protocol =   IPPROTO_IP,        /* wild card */
               .prot =       &raw_prot,
               .ops =        &inet_sockraw_ops,
               .capability = CAP_NET_RAW,
               .no_check =   UDP_CSUM_DEFAULT,
               .flags =      INET_PROTOSW_REUSE,
       }
};

数 组的每一个元素,就是支持的一种协议名称,例如IPOROTO_TCP,但是由于IPV4本身协议类型跟协议是一一对应的,所以没有更多的.type= SOCK_xxx了。这样数组实现了对PF_INET协议族下支持的协议类型,以及协议类型下边的协议进行了封装,虽然事实上它们是一一对应的关系,不过 理论上,完全可能存在一对多的可能。

数组内,封装的一个具体的协议,由struct proto结构来描述:

[Copy to clipboard][- ]CODE:/* Networking protocol blocks we attach to sockets.
* socket layer -> transport layer interface
* transport -> network interface is defined by struct inet_proto
*/
struct proto {
        void                        (*close)(struct sock *sk,
                                        long timeout);
        int                        (*connect)(struct sock *sk,
                                        struct sockaddr *uaddr,
                                        int addr_len);
        int                        (*disconnect)(struct sock *sk, int flags);

        struct sock *                (*accept) (struct sock *sk, int flags, int *err);

        int                        (*ioctl)(struct sock *sk, int cmd,
                                         unsigned long arg);
        int                        (*init)(struct sock *sk);
        int                        (*destroy)(struct sock *sk);
        void                        (*shutdown)(struct sock *sk, int how);
        int                        (*setsockopt)(struct sock *sk, int level,
                                        int optname, char __user *optval,
                                        int optlen);
        int                        (*getsockopt)(struct sock *sk, int level,
                                        int optname, char __user *optval,
                                        int __user *option);           
        int                        (*sendmsg)(struct kiocb *iocb, struct sock *sk,
                                           struct msghdr *msg, size_t len);
        int                        (*recvmsg)(struct kiocb *iocb, struct sock *sk,
                                           struct msghdr *msg,
                                        size_t len, int noblock, int flags,
                                        int *addr_len);
        int                        (*sendpage)(struct sock *sk, struct page *page,
                                        int offset, size_t size, int flags);
        int                        (*bind)(struct sock *sk,
                                        struct sockaddr *uaddr, int addr_len);

        int                        (*backlog_rcv) (struct sock *sk,
                                                struct sk_buff *skb);

        /* Keeping track of sk's, looking them up, and port selection methods. */
        void                        (*hash)(struct sock *sk);
        void                        (*unhash)(struct sock *sk);
        int                        (*get_port)(struct sock *sk, unsigned short snum);

        /* Memory pressure */
        void                        (*enter_memory_pressure)(void);
        atomic_t                *memory_allocated;        /* Current allocated memory. */
        atomic_t                *sockets_allocated;        /* Current number of sockets. */
        /*
         * Pressure flag: try to collapse.
         * Technical note: it is used by multiple contexts non atomically.
         * All the sk_stream_mem_schedule() is of this nature: accounting
         * is strict, actions are advisory and have some latency.
         */
        int                        *memory_pressure;
        int                        *sysctl_mem;
        int                        *sysctl_wmem;
        int                        *sysctl_rmem;
        int                        max_header;

        kmem_cache_t                *slab;
        unsigned int                obj_size;

        struct module                *owner;

        char                        name[32];

        struct list_head        node;

        struct {
                int inuse;
                u8  __pad[SMP_CACHE_BYTES - sizeof(int)];
        } stats[NR_CPUS];
};

以TCP协议为例,TCP协议的sokcet操作函数都被封装在这里了。

[Copy to clipboard][- ]CODE:struct proto tcp_prot = {
        .name                        = "TCP",
        .owner                        = THIS_MODULE,
        .close                        = tcp_close,
        .connect                = tcp_v4_connect,
        .disconnect                = tcp_disconnect,
        .accept                        = tcp_accept,
        .ioctl                        = tcp_ioctl,
        .init                        = tcp_v4_init_sock,
        .destroy                = tcp_v4_destroy_sock,
        .shutdown                = tcp_shutdown,
        .setsockopt                = tcp_setsockopt,
        .getsockopt                = tcp_getsockopt,
        .sendmsg                = tcp_sendmsg,
        .recvmsg                = tcp_recvmsg,
        .backlog_rcv                = tcp_v4_do_rcv,
        .hash                        = tcp_v4_hash,
        .unhash                        = tcp_unhash,
        .get_port                = tcp_v4_get_port,
        .enter_memory_pressure        = tcp_enter_memory_pressure,
        .sockets_allocated        = &tcp_sockets_allocated,
        .memory_allocated        = &tcp_memory_allocated,
        .memory_pressure        = &tcp_memory_pressure,
        .sysctl_mem                = sysctl_tcp_mem,
        .sysctl_wmem                = sysctl_tcp_wmem,
        .sysctl_rmem                = sysctl_tcp_rmem,
        .max_header                = MAX_TCP_HEADER,
        .obj_size                = sizeof(struct tcp_sock),
};








四、分配struct sock(sk)

浏览到这里,看完了PF_INET的协议簇、协议类型和协议(也就是socket调用的三个参数)的封装关系,它们通过了两个数据结构inet_protosw、struct proto来描述,被一个数组inetsw_array所封装。接下来看它的初始化工作:

[Copy to clipboard][- ]CODE:static struct list_head inetsw[SOCK_MAX];
static int __init inet_init(void)
{
……
        /* Register the socket-side information for inet_create. */
        for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
                INIT_LIST_HEAD(r);

        for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
                inet_register_protosw(q);
……
}

inetsw是一个数组,其每一个元素,都是一个链表首部,前面一个循环初始化之。后一个循环就值得注意了,也就是函数
inet_register_protosw:

[Copy to clipboard][- ]CODE:void inet_register_protosw(struct inet_protosw *p)
{
        struct list_head *lh;
        struct inet_protosw *answer;
        int protocol = p->protocol;
        struct list_head *last_perm;

        spin_lock_bh(&inetsw_lock);

        if (p->type >= SOCK_MAX)
                goto out_illegal;

        /* If we are trying to override a permanent protocol, bail. */
        answer = NULL;
        last_perm = &inetsw[p->type];
        list_for_each(lh, &inetsw[p->type]) {
                answer = list_entry(lh, struct inet_protosw, list);

                /* Check only the non-wild match. */
                if (INET_PROTOSW_PERMANENT & answer->flags) {
                        if (protocol == answer->protocol)
                                break;
                        last_perm = lh;
                }

                answer = NULL;
        }
        if (answer)
                goto out_permanent;

        /* Add the new entry after the last permanent entry if any, so that
         * the new entry does not override a permanent entry when matched with
         * a wild-card protocol. But it is allowed to override any existing
         * non-permanent entry.  This means that when we remove this entry, the
         * system automatically returns to the old behavior.
         */
        list_add_rcu(&p->list, last_perm);
out:
        spin_unlock_bh(&inetsw_lock);

        synchronize_net();

        return;

out_permanent:
        printk(KERN_ERR "Attempt to override permanent protocol %d.\n",
               protocol);
        goto out;

out_illegal:
        printk(KERN_ERR
               "Ignoring attempt to register invalid socket type %d.\n",
               p->type);
        goto out;
}

这 个函数完成的工作,就是把inetsw_array数组中,相同的协议类型下边的协议,加入到inetsw对应的协议类型的链表中去。因为事实上一对一的 关系,所以这个函数要简单得多:因为不存在其它成员,所以每一次list_entry都为空值,所以不存在覆盖和追加的情况,直接调用 list_add_rcu(&p->list, last_perm);把协议类型节点(struct inet_protosw 类型的数组的某个元素)添加到链表(链表首部本身是一个数组,数组索引是协议对应的协议类型的值)的第一个成员。

来做一个假设,如果SOCK_STREAM协议类型下边还有另一个协议,IPPROTO_123,那么inetsw_array数组中就会多出一个元素:

[Copy to clipboard][- ]CODE:        {
                .type =       SOCK_STREAM,
                .protocol =   IPPROTO_123,
                .prot =       &123_prot,
                ……
        },

这样,当遍历inetsw_array,再次进入inet_register_protosw函数后,因为SOCK_STREAM类型下已经注册了IPPROTO_TCP,所以,

[Copy to clipboard][- ]CODE:        list_for_each(lh, &inetsw[p->type]) {
                answer = list_entry(lh, struct inet_protosw, list);

                /* Check only the non-wild match. */
                if (INET_PROTOSW_PERMANENT & answer->flags) {
                        if (protocol == answer->protocol)        /* 已经注册了相同协议号,退出循环,因为没有置answer为NULL,所以后面会直接退出函数 */
                                break;
                        last_perm = lh;                                /* 移动位置指针,指向链表中最后一个元素 */
                }

                answer = NULL;
        }

这个循环, answer就会指向之前注册的TCP的链表节点,然后根据标志,如果是INET_PROTOSW_PERMANENT,则last_perm指向链表中 最后一个节点,也就是TCP,之后同样的道理,再把123追加到TCP之后,如果是INET_PROTOSW_REUSE,因为位置指针 last_perm没有移动,则之前注册的元素会被覆盖。

OK,绕了这么大一圈子,了解了协议的封装及链表的注册。现在回到inet_create中来:

[Copy to clipboard][- ]CODE:/*
*        Create an inet socket.
*/

static int inet_create(struct socket *sock, int protocol)
{
        struct sock *sk;
        struct list_head *p;
        struct inet_protosw *answer;
        struct inet_sock *inet;
        struct proto *answer_prot;
        unsigned char answer_flags;
        char answer_no_check;
        int err;
       
        sock->state = SS_UNCONNECTED;

socket的初始状态设置为“未连接”,这意味着面向连接的协议类型,如tcp,在使用之前必须建立连接修改状态位。

[Copy to clipboard][- ]CODE:        answer = NULL;
        rcu_read_lock();
        list_for_each_rcu(p, &inetsw[sock->type]) {
                answer = list_entry(p, struct inet_protosw, list);

                /* Check the non-wild match. */
                if (protocol == answer->protocol) {
                        if (protocol != IPPROTO_IP)
                                break;
                } else {
                        /* Check for the two wild cases. */
                        if (IPPROTO_IP == protocol) {
                                protocol = answer->protocol;
                                break;
                        }
                        if (IPPROTO_IP == answer->protocol)
                                break;
                }
                answer = NULL;
        }

这 个循环,根据socket(2)调用的protocol,把之前在链表中注册的协议节点找出来一个问题是,因为一一对应关系的存在,用户态调用 socket(2)的时候,常常第三个参数直接就置0了。也就是这里protocol 为 0。那内核又如何处理这一默认值呢?也就是 protocol != answer->protocol,而是被 if (IPPROTO_IP == protocol) 所匹配了。这样,将protocol置为链表中第一个协议。而当循环结束时,answer自然也是指向这个链表中的第一个注册节点。以刚才的例子, SOCK_STREAM下同时注册了TCP和123,那么这里默认就取TCP了。当然,把123在inetsw_array数组中的位置调前,那么就默认 取123了。

[Copy to clipboard][- ]CODE:        err = -ESOCKTNOSUPPORT;
        if (!answer)
                goto out_rcu_unlock;
        err = -EPERM;
        if (answer->capability > 0 && !capable(answer->capability))
                goto out_rcu_unlock;
        err = -EPROTONOSUPPORT;
        if (!protocol)
                goto out_rcu_unlock;
        /* 找到了组织,将创建的socket的ops函数指针集,指向协议类型的。例如创建的是SOCK_STREAM,那么就指向了inet_stream_ops */
        sock->ops = answer->ops;
        /* answer_prot指针指向了当前要创建的socket的协议类型下边的协议,如上例,它就是IPPROTO_TCP的tcp_prot结构 */
        answer_prot = answer->prot;
        answer_no_check = answer->no_check;
        answer_flags = answer->flags;
        rcu_read_unlock();

        BUG_TRAP(answer_prot->slab != NULL);

接下来一个重要的工作,就是为socket分配一个sock,并初始化它:

[Copy to clipboard][- ]CODE:        err = -ENOBUFS;
        sk = sk_alloc(PF_INET, GFP_KERNEL, answer_prot, 1);
        if (sk == NULL)
                goto out;

        err = 0;
        sk->sk_no_check = answer_no_check;
        if (INET_PROTOSW_REUSE & answer_flags)
                sk->sk_reuse = 1;

        inet = inet_sk(sk);

        if (SOCK_RAW == sock->type) {
                inet->num = protocol;
                if (IPPROTO_RAW == protocol)
                        inet->hdrincl = 1;
        }

        if (ipv4_config.no_pmtu_disc)
                inet->pmtudisc = IP_PMTUDISC_DONT;
        else
                inet->pmtudisc = IP_PMTUDISC_WANT;

        inet->id = 0;

        sock_init_data(sock, sk);

        sk->sk_destruct           = inet_sock_destruct;
        sk->sk_family           = PF_INET;
        sk->sk_protocol           = protocol;
        sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;

        inet->uc_ttl        = -1;
        inet->mc_loop        = 1;
        inet->mc_ttl        = 1;
        inet->mc_index        = 0;
        inet->mc_list        = NULL;

#ifdef INET_REFCNT_DEBUG
        atomic_inc(&inet_sock_nr);
#endif

        if (inet->num) {
                /* It assumes that any protocol which allows
                 * the user to assign a number at socket
                 * creation time automatically
                 * shares.
                 */
                inet->sport = htons(inet->num);
                /* Add to protocol hash chains. */
                sk->sk_prot->hash(sk);
        }

        if (sk->sk_prot->init) {
                err = sk->sk_prot->init(sk);
                if (err)
                        sk_common_release(sk);
        }
out:
        return err;
out_rcu_unlock:
        rcu_read_unlock();
        goto out;
}

虽然create的代码就到这儿了,不过要说清楚sk的分配,还得费上大力气。
每一个Socket套接字,都有一个对应的struct socket结构来描述(内核中一般使用名称为sock),但是同时又一个struct sock结构(内核中一般使用名称为sk)。两者之间是一一对应的关系。在后面的sock_init_data函数中,可以看到

[Copy to clipboard][- ]CODE:sk->sk_socket                =        sock;
sock->sk        =        sk;

这样的代码。

socket结构和sock结构实际上是同一个事物的两个方面。不妨说,socket结构是面向进程和系统调用界面的侧面,而sock结构则是面向底层驱 动程序的侧面。设计者把socket套接字中,与文件系统关系比较密切的那一部份放在socket结构中,而把与通信关系比较密切的那一部份,则单独成为 一个数结结构,那就是sock结构。由于这两部份逻辑上本来就是一体的,所以要通过指针互相指向对方,形成一对一的关系。

再暂时回到inet_init中来,初始化工作中,有如下代码:

[Copy to clipboard][- ]CODE:        rc = proto_register(&tcp_prot, 1);
        if (rc)
                goto out;

        rc = proto_register(&udp_prot, 1);
        if (rc)
                goto out_unregister_tcp_proto;

        rc = proto_register(&raw_prot, 1);
        if (rc)
                goto out_unregister_udp_proto;

这里为每个protocol都调用了proto_register函数,其重要功能之一,就是根据协议的obj_size成员的大小,为协议创建高速缓存:

[Copy to clipboard][- ]CODE:static DEFINE_RWLOCK(proto_list_lock);
static LIST_HEAD(proto_list);

int proto_register(struct proto *prot, int alloc_slab)
{
        int rc = -ENOBUFS;

        if (alloc_slab) {
        /* 可以看到,函数最重要的功能就是根据prot的obj_size成员的大小,为协议创建高速缓存 */       
                prot->slab = kmem_cache_create(prot->name, prot->obj_size, 0,
                if (prot->slab == NULL) {
                        printk(KERN_CRIT "%s: Can't create sock SLAB cache!\n",
                               prot->name);
                        goto out;
                }
        }

        /* 顺便看到它的另一个重要的功能,是维护一个以proto_list为首的链表 */
        write_lock(&proto_list_lock);
        list_add(&prot->node, &proto_list);
        write_unlock(&proto_list_lock);
        rc = 0;
out:
        return rc;
}

这里要注意的是:prot->obj_size的大小,它它非仅仅是一个sk的大小!!!以TCP为例:.obj_size = sizeof(struct tcp_sock)。稍后再来分析这个东东。

回到inet_create()函数中来,其调用sk_alloc()分配一个sk:

[Copy to clipboard][- ]CODE:sk = sk_alloc(PF_INET, GFP_KERNEL, answer_prot, 1);


[Copy to clipboard][- ]CODE:struct sock *sk_alloc(int family, int priority, struct proto *prot, int zero_it)
{
        struct sock *sk = NULL;
        kmem_cache_t *slab = prot->slab;

        if (slab != NULL)
                sk = kmem_cache_alloc(slab, priority);
        else
                sk = kmalloc(prot->obj_size, priority);

        if (sk) {
                if (zero_it) {
                        memset(sk, 0, prot->obj_size);
                        sk->sk_family = family;
                        /*
                         * See comment in struct sock definition to understand
                         * why we need sk_prot_creator -acme
                         */
                        sk->sk_prot = sk->sk_prot_creator = prot;
                        ……

在之前创建的高速缓存中申请分配一个slab缓存项。并清零。然后设置协议族、并把sk中的sk_prot与对应的协议关联起来。

五、初始化sk

分配完成sk后,另一个重要的功能就是初始化它,sk的成员相当复杂,其主要的初始化工作是在函数sock_init_data()中完成的:

[Copy to clipboard][- ]CODE:void sock_init_data(struct socket *sock, struct sock *sk)
{
        /* 初始化其三个队列 */
        skb_queue_head_init(&sk->sk_receive_queue);
        skb_queue_head_init(&sk->sk_write_queue);
        skb_queue_head_init(&sk->sk_error_queue);

        sk->sk_send_head        =        NULL;

        /* 初始化数据包发送定时器 */
        init_timer(&sk->sk_timer);
       
        sk->sk_allocation        =        GFP_KERNEL;
        sk->sk_rcvbuf                =        sysctl_rmem_default;
        sk->sk_sndbuf                =        sysctl_wmem_default;
        sk->sk_state                =        TCP_CLOSE;
        /* 指向对应的socket结构 */
        sk->sk_socket                =        sock;

        sock_set_flag(sk, SOCK_ZAPPED);

        if(sock)
        {
                sk->sk_type        =        sock->type;
                sk->sk_sleep        =        &sock->wait;
                /* 回指对应的scok结构 */
                sock->sk        =        sk;
        } else
                sk->sk_sleep        =        NULL;

        rwlock_init(&sk->sk_dst_lock);
        rwlock_init(&sk->sk_callback_lock);

        sk->sk_state_change        =        sock_def_wakeup;
        sk->sk_data_ready        =        sock_def_readable;
        sk->sk_write_space        =        sock_def_write_space;
        sk->sk_error_report        =        sock_def_error_report;
        sk->sk_destruct                =        sock_def_destruct;

        sk->sk_sndmsg_page        =        NULL;
        sk->sk_sndmsg_off        =        0;

        sk->sk_peercred.pid         =        0;
        sk->sk_peercred.uid        =        -1;
        sk->sk_peercred.gid        =        -1;
        sk->sk_write_pending        =        0;
        sk->sk_rcvlowat                =        1;
        sk->sk_rcvtimeo                =        MAX_SCHEDULE_TIMEOUT;
        sk->sk_sndtimeo                =        MAX_SCHEDULE_TIMEOUT;

        sk->sk_stamp.tv_sec     = -1L;
        sk->sk_stamp.tv_usec    = -1L;

        atomic_set(&sk->sk_refcnt, 1);
}

sock结构中,有三个重要的双向队列,分别是sk_receive_queue、sk_write_queue和sk_error_queue。从它们的名字就可以看出来其作用了。
队列并非采用通用的list_head来维护,而是使用skb_buffer队列:

[Copy to clipboard][- ]CODE:struct sk_buff_head {
        /* These two members must be first. */
        struct sk_buff        *next;
        struct sk_buff        *prev;

        __u32                qlen;
        spinlock_t        lock;
};

这样,队列中指向的每一个skb_buffer,就是一个数据包,分别是接收、发送和投递错误。

剩余的就是初始化其它成员变量了。后面再来专门分析这些成员的作用。

inet_create函数中,除了初始化sk成员的值,还有一部份代码,是初始化一个inet的东东:

[Copy to clipboard][- ]CODE:                inet = inet_sk(sk);
        inet->uc_ttl        = -1;
        inet->mc_loop        = 1;
        inet->mc_ttl        = 1;
        inet->mc_index        = 0;
        inet->mc_list        = NULL;

inet是一个struct inet_sock结构类型,来看它的定义:

[Copy to clipboard][- ]CODE:struct inet_sock {
        /* sk and pinet6 has to be the first two members of inet_sock */
        struct sock                sk;
               ……
}

只留意它的第一个成员就足够了。

我们说sock是面向用户态调用,而sk是面向内核驱动调用的,那sk是如何与协议栈交互的呢?对于每一个类型的协议,为了与sk联系起来,都定义了一个struct XXX_sock结构,XXX是协议名,例如:

[Copy to clipboard][- ]CODE:struct tcp_sock {
        /* inet_sock has to be the first member of tcp_sock */
        struct inet_sock        inet;
        int        tcp_header_len;        /* Bytes of tcp header to send                */
        ……
}

struct udp_sock {
        /* inet_sock has to be the first member */
        struct inet_sock inet;
        int                 pending;        /* Any pending frames ? */
        unsigned int         corkflag;        /* Cork is required */
          __u16                 encap_type;        /* Is this an Encapsulation socket? */
        /*
         * Following member retains the infomation to create a UDP header
         * when the socket is uncorked.
         */
        __u16                 len;                /* total length of pending frames */
};

struct raw_sock {
        /* inet_sock has to be the first member */
        struct inet_sock   inet;
        struct icmp_filter filter;
};

很明显,它们的结构定构是“af_inet一般属性+自己的私有属性”,因为它们的第一个成员总是inet。

呵呵,现在回头来照一下起初在af_inet.c中,封装协议注册的时候,size成员,对于tcp而言:
.obj_size                = sizeof(struct tcp_sock),
其它协议类似。

以obj_size来确定每个slab缓存项分配的大小,所以,我们就可说,每次申请分配的,实际上是一个struct XXX_sock结构大小的结构。因为都是定义于上层结构的第一个成员,可以使用强制类型转换来使用这块分配的内存空间。例如:

[Copy to clipboard][- ]CODE:inet = inet_sk(sk);

static inline struct inet_sock *inet_sk(const struct sock *sk)
{
        return (struct inet_sock *)sk;
}

struct tcp_sock *tp = tcp_sk(sk);

static inline struct tcp_sock *tcp_sk(const struct sock *sk)
{
        return (struct tcp_sock *)sk;
}

OK,inet_create()运行完,一个socket套接字基本上就创建完毕了,剩下的就是与文件系统挂钩,回到最初的 sys_socket()函数中来,它在调用完sock_create()后,紧接着调用sock_map_fd()函数:

[Copy to clipboard][- ]CODE:int sock_map_fd(struct socket *sock)
{
        int fd;
        struct qstr this;
        char name[32];

        /*
         *        Find a file descriptor suitable for return to the user.
         */

        fd = get_unused_fd();
        if (fd >= 0) {
                struct file *file = get_empty_filp();

                if (!file) {
                        put_unused_fd(fd);
                        fd = -ENFILE;
                        goto out;
                }

                sprintf(name, "[%lu]", SOCK_INODE(sock)->i_ino);
                this.name = name;
                this.len = strlen(name);
                this.hash = SOCK_INODE(sock)->i_ino;

                file->f_dentry = d_alloc(sock_mnt->mnt_sb->s_root, &this);
                if (!file->f_dentry) {
                        put_filp(file);
                        put_unused_fd(fd);
                        fd = -ENOMEM;
                        goto out;
                }
                file->f_dentry->d_op = &sockfs_dentry_operations;
                d_add(file->f_dentry, SOCK_INODE(sock));
                file->f_vfsmnt = mntget(sock_mnt);
                file->f_mapping = file->f_dentry->d_inode->i_mapping;

                sock->file = file;
                file->f_op = SOCK_INODE(sock)->i_fop = &socket_file_ops;
                file->f_mode = FMODE_READ | FMODE_WRITE;
                file->f_flags = O_RDWR;
                file->f_pos = 0;
                fd_install(fd, file);
        }

out:
        return fd;
}

这个函数的核心思想,在一开始, 就已经分析过了。从进程的角度来讲,一个socket套接字就是一个特殊的,已打开的文件。前面分配好一个socket后,这里要做的就是将它与文件系统 拉上亲戚关系。首先获取一个空闲的文件描述符号和file结构。然后在文件系统中分配一个目录项(d_alloc),使其指向已经分配的inode节点 (d_add),然后把其目录项挂在sockfs文件系统的根目录之下。并且把目录项的指针d_op设置成指向 sockfs_dentry_operati,这个数据结构通过函数指针提供他与文件路径有关的操作:

[Copy to clipboard][- ]CODE:static struct dentry_operations sockfs_dentry_operations = {
        .d_delete =        sockfs_delete_dentry,
};

最后一步,就是将file结构中的f_op和sock结构中的i_fop都指向socket_file_ops,它是一个函数指针集,指向了socket面向文件系统的用户态调用的一些接口函数:

[Copy to clipboard][- ]CODE:static struct file_operations socket_file_ops = {
        .owner =        THIS_MODULE,
        .llseek =        no_llseek,
        .aio_read =        sock_aio_read,
        .aio_write =        sock_aio_write,
        .poll =                sock_poll,
        .unlocked_ioctl = sock_ioctl,
        .mmap =                sock_mmap,
        .open =                sock_no_open,        /* special open code to disallow open via /proc */
        .release =        sock_close,
        .fasync =        sock_fasync,
        .readv =        sock_readv,
        .writev =        sock_writev,
        .sendpage =        sock_sendpage
};

OK,到这里,整个socket套接字的创建工作,就宣告完成了。


写到这里,可以为socket的创建下一个小结了:
1、所谓创建socket,对内核而言,最重要的工作就是分配sock与sk;
2、sock面向上层系统调用,主要是与文件系统交互。通过进程的current指针的files,结合创建socket时返回的文件描符述,可以找到内 核中对应的struct file,再根据file的f_dentry可以找到对应的目录项,而目录项struct dentry中,有d_inode指针,指向与sock封装在一起的inode。sock又与sk指针互指,一一对应。在这串结构中,有两个重要的函数集 指针,一个是文件系统struct file中的f_op指针,它指向了,对应的用户态调用的read,write等操调用,但不支持open,另一个是struct socket结构,即sock的ops指针,它在inet_create()中被置为
sock->ops = answer->ops;
指向具体协议类型的ops;例如,inet_stream_ops、inet_dgram_ops或者是inet_sockraw_ops等等。它用来支持上层的socket的其它API调用。
3、sk面向内核协议栈,协议栈与它的接口数据结构是struct protoname_sock,该结构中包含了一般性的inet结构和自己的私有成员,struct inet_sock的第一个成员就是一个sk指针,而分配的sk,实际上空间大小是struct protoname_sock,所以,这三者可以通过强制类型转换来获取需要的指针。
4、由于水平有限,文件系统的一些细节被我跳过了,sk中的大多数成员变量的作用,也被我跳出过了。呵呵,还好,终于还是把这块流程给初步分析出来了。另外,当时写的时候,没有想到会写这么长,大大超出了每贴的字限制。所以,每个小节内容跟标题可能会有点对不上号。








第二部份 Socket的bind(2),绑定地址

1、bind(2)

当创建了一个Socket套接字后,对于服务器来说,接下来的工作,就是调用bind(2)为服务器指明本地址、协议端口号,常常可以看到这样的代码:
strut sockaddr_in sin;

sin.sin_family = AF_INET;
sin.sin_addr.s_addr = xxx;
sin.sin_port = xxx;

bind(sock, (struct sockaddr *)&sin, sizeof(sin));

从这个系统调用中,可以知道当进行SYS_BIND 操作的时候,:
1、对于AF_INET协议簇来讲,其地址格式是strut sockaddr_in,而对于socket来讲,strut sockaddr 结构表示的地址格式实现了更高层次
的抽像,因为每种协议长簇的地址不一定是相同的,所以,系统调用的第三个参数得指明该协议簇的地址格式的长度。

2、进行bind(2)系统调用时,除了地址长度外,还得向内核提供:sock描述符、协议簇名称、本地地址、端口这些参数;

2、sys_bind()
操作SYS_BIND 是由sys_bind()实现的:

asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
        struct socket *sock;
        char address[MAX_SOCK_ADDR];
        int err;

        if((sock = sockfd_lookup(fd,&err))!=NULL)
        {
                if((err=move_addr_to_kernel(umyaddr,addrlen,address))>=0) {
                        err = security_socket_bind(sock, (struct sockaddr *)address, addrlen);
                        if (err) {
                                sockfd_put(sock);
                                return err;
                        }
                        err = sock->ops->bind(sock, (struct sockaddr *)address, addrlen);
                }
                sockfd_put(sock);
        }                       
        return err;
}

在socket的创建中,已经反复分析了socket与文件系统的关系,现在已知socket的描述符号,要找出与之相关的socket结构,应该是件容易的事情:

struct socket *sockfd_lookup(int fd, int *err)
{
        struct file *file;
        struct inode *inode;
        struct socket *sock;

        if (!(file = fget(fd)))
        {
                *err = -EBADF;
                return NULL;
        }

        inode = file->f_dentry->d_inode;
        if (!S_ISSOCK(inode->i_mode)) {
                *err = -ENOTSOCK;
                fput(file);
                return NULL;
        }

        sock = SOCKET_I(inode);
        if (sock->file != file) {
                printk(KERN_ERR "socki_lookup: socket file changed!\n");
                sock->file = file;
        }
        return sock;
}

fget从当前进程的files指针中,根据sock对应的描述符号,找到已打开的文件file,再根据文件的目录项中的inode,利用inode与 sock被封装在同一个结构中的事实,调用宏SOCKET_I找到待查的sock结构。最后做一个小小的判断,因为正常情况下,sock的file指针是 回指与之相关的file。

接下来的工作是把用户态的地址拷贝至内核中来:
int move_addr_to_kernel(void __user *uaddr, int ulen, void *kaddr)
{
        if(ulen<0||ulen>MAX_SOCK_ADDR)
                return -EINVAL;
        if(ulen==0)
                return 0;
        if(copy_from_user(kaddr,uaddr,ulen))
                return -EFAULT;
        return 0;
}

bind(2)第三个参数必须存在的原因之一,copy_from_user必须知道拷贝的字节长度。

因为sock的ops函数指针集,在创建之初,就指向了对应的协议类型,例如如果类型是SOCK_STREAM,那么它就指向inetsw_array[0].ops。也就是inet_stream_ops:
struct proto_ops inet_stream_ops = {
        .family =        PF_INET,
        ……
        .bind =                inet_bind,
        ……
};

sys_bind()在做完了一个通用的socket bind应该做的事情,包括查找对应sock结构,拷贝地址。就调用对应协议族的对应协议类型的bind函数,也就是inet_bind。
3、inet_bind
说bind(2)的最重要的作用,就是为套接字绑定地址和端口,那么要分析inet_bind()之前,要搞清楚的一件事情就是,这个绑定,是绑定到哪儿?或者说,是绑定到内核的哪个数据结构的哪个成员变量上面??
有三个地方是可以考虑的:socket结构,包括sock和sk,inet结构,以及protoname_sock结构。绑定在socket结构上是可行 的,这样可以实现最高层面上的抽像,但是因为每一类协议簇socket的地址及端口表现形式差异很大,这样就得引入专门的转换处理功能。绑定在 protoname_sock也是可行的,但是却是最笨拙的,因为例如tcp和udp,它们的地址及端口表现形式是一样的,这样就浪费了空间,加大了代码 处理量。所以,inet做为一个协议类型的抽像,是最理想的地方了,再来回顾一下它的定义:

[Copy to clipboard][- ]CODE:struct inet_sock {
        ……
        /* Socket demultiplex comparisons on incoming packets. */
        __u32                        daddr;                /* Foreign IPv4 addr */
        __u32                        rcv_saddr;        /* Bound local IPv4 addr */
        __u16                        dport;                /* Destination port */
        __u16                        num;                /* Local port */
        __u32                        saddr;                /* Sending source */
        ……
};

去掉了其它成员,保留了与地址及端口相关的成员变量,从注释中,可以清楚地了解它们的作用。所以,我们说的bind(2)之绑定,主要就是对这几个成员变量赋值的过程了。

[Copy to clipboard][- ]CODE:int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
        /* 获取地址参数 */
        struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
        /* 获取sock对应的sk */
        struct sock *sk = sock->sk;
        /* 获取sk对应的inet */
        struct inet_sock *inet = inet_sk(sk);
        /* 这个临时变量用来保存用户态传递下来的端口参数 */
        unsigned short snum;
        int chk_addr_ret;
        int err;

        /* 如果协议簇对应的协议自身还有bind函数,调用之,例如SOCK_RAW就还有一个raw_bind */
        if (sk->sk_prot->bind) {
                err = sk->sk_prot->bind(sk, uaddr, addr_len);
                goto out;
        }
        /* 校验地址长度 */
        err = -EINVAL;
        if (addr_len < sizeof(struct sockaddr_in))
                goto out;

        /* 判断地址类型:广播?多播?单播? */
        chk_addr_ret = inet_addr_type(addr->sin_addr.s_addr);
       
        /* ipv4有一个ip_nonlocal_bind标志,表示是否绑定非本地址IP地址,可以通过
         * cat /proc/sys/net/ipv4/ip_nonlocal_bind查看到。它用来解决某些服务绑定
         * 动态IP地址的情况。作者在注释中已有详细说明:
         * Not specified by any standard per-se, however it breaks too
         * many applications when removed.  It is unfortunate since
         * allowing applications to make a non-local bind solves
         * several problems with systems using dynamic addressing.
         * (ie. your servers still start up even if your ISDN link
         *  is temporarily down)
         * 这里判断,用来确认如果没有开启“绑定非本地址IP”,地址值及类型是正确的
         */
        err = -EADDRNOTAVAIL;
        if (!sysctl_ip_nonlocal_bind &&
            !inet->freebind &&
            addr->sin_addr.s_addr != INADDR_ANY &&
            chk_addr_ret != RTN_LOCAL &&
            chk_addr_ret != RTN_MULTICAST &&
            chk_addr_ret != RTN_BROADCAST)
                goto out;
       
        /* 获取协议端口号 */
        snum = ntohs(addr->sin_port);
        err = -EACCES;
        /* 校验当前进程有没有使用低于1024端口的能力 */
        if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))
                goto out;

        /*      We keep a pair of addresses. rcv_saddr is the one
         *      used by hash lookups, and saddr is used for transmit.
         *
         *      In the BSD API these are the same except where it
         *      would be illegal to use them (multicast/broadcast) in
         *      which case the sending device address is used.
         */
        lock_sock(sk);

        /* 检查socket是否已经被绑定过了:用了两个检查项,一个是sk状态,另一个是是否已经绑定过端口了
        当然地址本来就可以为0,所以,不能做为检查项 */
        err = -EINVAL;
        if (sk->sk_state != TCP_CLOSE || inet->num)
                goto out_release_sock;
       
        /* 绑定inet的接收地址(地址服务绑定地址)和来源地址为用户态指定地址 */
        inet->rcv_saddr = inet->saddr = addr->sin_addr.s_addr;
        /* 若地址类型为广播或多播,则将地址置0,表示直接使用网络设备 */
        if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
                inet->saddr = 0;  /* Use device */

        /*
         * 调用协议的get_port函数,确认是否可绑定端口,若可以,则绑定在inet->num之上,注意,这里虽然没有
         * 把inet传过去,但是第一个参数sk,它本身和inet是可以互相转化的
         */
        if (sk->sk_prot->get_port(sk, snum)) {
                inet->saddr = inet->rcv_saddr = 0;
                err = -EADDRINUSE;
                goto out_release_sock;
        }
       
        /* 如果端口和地址可以绑定,置标志位 */
        if (inet->rcv_saddr)
                sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
        if (snum)
                sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
        /* inet的sport(来源端口)成员也置为绑定端口 */
        inet->sport = htons(inet->num);
        inet->daddr = 0;
        inet->dport = 0;
        sk_dst_reset(sk);
        err = 0;
out_release_sock:
        release_sock(sk);
out:
        return err;
}

上述分析中,忽略的第一个细节是capable()函数调用,它是 Linux 安全模块(LSM)的一部份,简单地讲其用来对权限做出检查,
检查是否有权对指定的资源进行操作。这里它的参数是CAP_NET_BIND_SERVICE,表示的含义是:

[Copy to clipboard][- ]CODE:/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */

#define CAP_NET_BIND_SERVICE 10

另一个就是协议的端口绑定,调用了协议的get_port函数,如果是SOCK_STREAM的TCP 协议,那么它就是tcp_v4_get_port()函数。

4、协议端口的绑定

要分配这个函数,还是得先绕一些基本的东东。这里涉及到内核中提供hash链表的操作的API。可以参考其它相关资料。
http://www.ibm.com/developerwork ... /l-chain/index.html
这里讲了链表的实现,顺道提了一个hash链表,觉得写得还不错,收藏一下。

对于TCP已注册的端口,是采用一个hash表来维护的。hash桶用struct tcp_bind_hashbucket结构来表示:

[Copy to clipboard][- ]CODE:struct tcp_bind_hashbucket {
        spinlock_t                lock;
        struct hlist_head        chain;
};

hash表中的每一个hash节点,用struct tcp_bind_hashbucket结构来表示:
struct tcp_bind_bucket {

[Copy to clipboard][- ]CODE:        unsigned short                port;                        /* 节点中绑定的端口 */
        signed short                fastreuse;
        struct hlist_node        node;
        struct hlist_head        owners;
};

tcp_hashinfo的hash表信息,都集中封装在结构tcp_hashinfo当中,而维护已注册端口,只是它其中一部份:

[Copy to clipboard][- ]CODE:extern struct tcp_hashinfo {
        ……

        /* Ok, let's try this, I give up, we do need a local binding
         * TCP hash as well as the others for fast bind/connect.
         */
        struct tcp_bind_hashbucket *__tcp_bhash;

        int __tcp_bhash_size;
        ……
} tcp_hashinfo;

#define tcp_bhash        (tcp_hashinfo.__tcp_bhash)
#define tcp_bhash_size        (tcp_hashinfo.__tcp_bhash_size)

其使用的hash函数是tcp_bhashfn:

[Copy to clipboard][- ]CODE:/* These are AF independent. */
static __inline__ int tcp_bhashfn(__u16 lport)
{
        return (lport & (tcp_bhash_size - 1));
}

这样,如果要取得某个端口对应的hash链的首部hash桶节点的话,可以使用:

[Copy to clipboard][- ]CODE:struct tcp_bind_hashbucket *head;
head = &tcp_bhash[tcp_bhashfn(snum)];

如果要新绑定一个端口,就是先创建一个struct tcp_bind_hashbucket结构的hash节点,然后把它插入到对应的
hash链中去:

[Copy to clipboard][- ]CODE:struct tcp_bind_bucket *tb;

tb = tcp_bucket_create(head, snum);

struct tcp_bind_bucket *tcp_bucket_create(struct tcp_bind_hashbucket *head,
                                          unsigned short snum)
{
        struct tcp_bind_bucket *tb = kmem_cache_alloc(tcp_bucket_cachep,
                                                      SLAB_ATOMIC);
        if (tb) {
                tb->port = snum;
                tb->fastreuse = 0;
                INIT_HLIST_HEAD(&tb->owners);
                hlist_add_head(&tb->node, &head->chain);
        }
        return tb;
}

另外,sk中,还级护了一个类似的hash链表,同时需要调用tcp_bind_hash()函数把hash节点插入进去:

[Copy to clipboard][- ]CODE:struct sock {
        struct sock_common        __sk_common;
#define sk_bind_node                __sk_common.skc_bind_node
        ……
}



[Copy to clipboard][- ]CODE:/* @skc_bind_node: bind hash linkage for various protocol lookup tables */
struct sock_common {
        struct hlist_node        skc_bind_node;
        ……
}



[Copy to clipboard][- ]CODE:if (!tcp_sk(sk)->bind_hash)
        tcp_bind_hash(sk, tb, snum);
       
void tcp_bind_hash(struct sock *sk, struct tcp_bind_bucket *tb,
                   unsigned short snum)
{
        inet_sk(sk)->num = snum;
        sk_add_bind_node(sk, &tb->owners);
        tcp_sk(sk)->bind_hash = tb;
}

这里,就顺道绑定了inet的num成员变量,并置协议的bind_hash指针为当前分配的hash节点。而sk_add_bind_node函数,
就是一个插入hash表节点的过程:

[Copy to clipboard][- ]CODE:static __inline__ void sk_add_bind_node(struct sock *sk,
                                        struct hlist_head *list)
{
        hlist_add_head(&sk->sk_bind_node, list);
}

如果要遍历hash表的话,例如在插入之前,先判断端口是否已经在hash表当中了。就可以调用:

[Copy to clipboard][- ]CODE:#define tb_for_each(tb, node, head) hlist_for_each_entry(tb, node, head, node)

struct tcp_bind_hashbucket *head;
struct tcp_bind_bucket *tb;

head = &tcp_bhash[tcp_bhashfn(snum)];
spin_lock(&head->lock);
tb_for_each(tb, node, &head->chain)
        if (tb->port == snum)
                found,do_something;

有了这些基础知识,再来看tcp_v4_get_port()的实现,就要容易得多了:

[Copy to clipboard][- ]CODE:static int tcp_v4_get_port(struct sock *sk, unsigned short snum)
{
        struct tcp_bind_hashbucket *head;
        struct hlist_node *node;
        struct tcp_bind_bucket *tb;
        int ret;

        local_bh_disable();
       
        /* 如果端口值为0,意味着让系统从本地可用端口用选择一个,并置snum为分配的值 */
        if (!snum) {
                int low = sysctl_local_port_range[0];
                int high = sysctl_local_port_range[1];
                int remaining = (high - low) + 1;
                int rover;

                spin_lock(&tcp_portalloc_lock);
                if (tcp_port_rover < low)
                        rover = low;
                else
                        rover = tcp_port_rover;
                do {
                        rover++;
                        if (rover > high)
                                rover = low;
                        head = &tcp_bhash[tcp_bhashfn(rover)];
                        spin_lock(&head->lock);
                        tb_for_each(tb, node, &head->chain)
                                if (tb->port == rover)
                                        goto next;
                        break;
                next:
                        spin_unlock(&head->lock);
                } while (--remaining > 0);
                tcp_port_rover = rover;
                spin_unlock(&tcp_portalloc_lock);

                /* Exhausted local port range during search? */
                ret = 1;
                if (remaining <= 0)
                        goto fail;

                /* OK, here is the one we will use.  HEAD is
                 * non-NULL and we hold it's mutex.
                 */
                snum = rover;
        } else {
                /* 否则,就在hash表中,查找端口是否已经存在 */
                head = &tcp_bhash[tcp_bhashfn(snum)];
                spin_lock(&head->lock);
                tb_for_each(tb, node, &head->chain)
                        if (tb->port == snum)
                                goto tb_found;
        }
        tb = NULL;
        goto tb_not_found;
tb_found:
        /*  稍后有对应的代码:第一次分配tb后,会调用tcp_bind_hash加入至相应的sk,这里先做一个判断,来确定这一步工作是否进行过*/
        if (!hlist_empty(&tb->owners)) {
/* socket的SO_REUSEADDR 选项,用来确定是否允许本地地址重用,例如同时启动多个服务器、多个套接字绑定至同一端口等等,sk_reuse成员对应其值,因为如果一个绑定的 hash节点已经存在,而且不允许重用的话,那么则表示因冲突导致出错,调用tcp_bind_conflict来处理之 */
                if (sk->sk_reuse > 1)
                        goto success;
                if (tb->fastreuse > 0 &&
                    sk->sk_reuse && sk->sk_state != TCP_LISTEN) {
                        goto success;
                } else {
                        ret = 1;
                        if (tcp_bind_conflict(sk, tb))
                                goto fail_unlock;
                }
        }
tb_not_found:
        /* 如果不存在,则分配hash节点,绑定端口 */
        ret = 1;
        if (!tb && (tb = tcp_bucket_create(head, snum)) == NULL)
                goto fail_unlock;
        if (hlist_empty(&tb->owners)) {
                if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
                        tb->fastreuse = 1;
                else
                        tb->fastreuse = 0;
        } else if (tb->fastreuse &&
                   (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
                tb->fastreuse = 0;
success:
        if (!tcp_sk(sk)->bind_hash)
                tcp_bind_hash(sk, tb, snum);
        BUG_TRAP(tcp_sk(sk)->bind_hash == tb);
        ret = 0;

fail_unlock:
        spin_unlock(&head->lock);
fail:
        local_bh_enable();
        return ret;
}

到这里,可以为这部份下一个小结了,所谓绑定,就是:
1、设置内核中inet相关变量成员的值,以待后用;
2、协议中,如TCP协议,记录绑定的协议端口的信息,采用hash链表存储,sk中也同时维护了这么一个链表。两者的区别应该是
前者给协议用。后者给socket用。








Linux TCP/IP协议栈之Socket的实现分析(五 Connect客户端发起连接请求)  




1、sys_connect

对于客户端来说,当创建了一个套接字后,就可以连接它了。

[Copy to clipboard][- ]CODE:                case SYS_CONNECT:
                        err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
                        break;



[Copy to clipboard][- ]CODE:asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
        struct socket *sock;
        char address[MAX_SOCK_ADDR];
        int err;

        sock = sockfd_lookup(fd, &err);
        if (!sock)
                goto out;
        err = move_addr_to_kernel(uservaddr, addrlen, address);
        if (err < 0)
                goto out_put;

        err = security_socket_connect(sock, (struct sockaddr *)address, addrlen);
        if (err)
                goto out_put;

        err = sock->ops->connect(sock, (struct sockaddr *) address, addrlen,
                                 sock->file->f_flags);
out_put:
        sockfd_put(sock);
out:
        return err;
}

跟其它操作类似,sys_connect接着调用inet_connect:

[Copy to clipboard][- ]CODE:/*
*        Connect to a remote host. There is regrettably still a little
*        TCP 'magic' in here.
*/
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
                        int addr_len, int flags)
{
        struct sock *sk = sock->sk;
        int err;
        long timeo;

        lock_sock(sk);

        if (uaddr->sa_family == AF_UNSPEC) {
                err = sk->sk_prot->disconnect(sk, flags);
                sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
                goto out;
        }

提交的协议簇不正确,则断开连接。

[Copy to clipboard][- ]CODE:        switch (sock->state) {
        default:
                err = -EINVAL;
                goto out;
        case SS_CONNECTED:
                err = -EISCONN;
                goto out;
        case SS_CONNECTING:
                err = -EALREADY;
                /* Fall out of switch with err, set for this state */
                break;

socket处于不正确的连接状态,返回相应的错误值。

[Copy to clipboard][- ]CODE:        case SS_UNCONNECTED:
                err = -EISCONN;
                if (sk->sk_state != TCP_CLOSE)
                        goto out;
                /*调用协议的连接函数*/
                err = sk->sk_prot->connect(sk, uaddr, addr_len);
                if (err < 0)
                        goto out;
                /*协议方面的工作已经处理完成了,但是自己的一切工作还没有完成,所以切换至正在连接中*/
                  sock->state = SS_CONNECTING;

                /* Just entered SS_CONNECTING state; the only
                 * difference is that return value in non-blocking
                 * case is EINPROGRESS, rather than EALREADY.
                 */
                err = -EINPROGRESS;
                break;
        }

对于TCP的实际的连接,是通过调用tcp_v4_connect()函数来实现的。

二、tcp_v4_connect函数
对于TCP协议来说,其连接,实际上就是发送一个SYN报文,在服务器的应到到来时,回答它一个ack报文,也就是完成三次握手中的第一和第三次。

要发送SYN报文,也就是说,需要有完整的来源/目的地址,来源/目的端口,目的地址/端口由用户态提交,但是问题是没有自己的地址和端口,因为并没有调用过bind(2),一台主机,对于端口,可以像sys_bind()那样,从本地未用端口中动态分配一个,那地址呢?因为一台主机可能会存在多个IP地址,如果随机动态选择,那么有可能选择一个错误的来源地址,将不能正确地到达目的地址。换句话说,来源地址的选择,是与路由相关的。

调用路由查找的核心函数ip_route_output_slow(),在没有提供来源地址的情况下,会根据实际情况,调用inet_select_addr()函数来选择一个合适的。同时,如果路由查找命中,会生成一个相应的路由缓存项,这个缓存项,不但对当前发送SYN报文有意义,对于后续的所有数据包,都可以起到一个加速路由查找的作用。这一任务,是通过ip_route_connect()函数完成的,它返回相应的路由缓存项(也就是说,来源地址也在其中了):

[Copy to clipboard][- ]CODE:static inline int ip_route_connect(struct rtable **rp, u32 dst,
                                   u32 src, u32 tos, int oif, u8 protocol,
                                   u16 sport, u16 dport, struct sock *sk)
{
        struct flowi fl = { .oif = oif,
                            .nl_u = { .ip4_u = { .daddr = dst,
                                                 .saddr = src,
                                                 .tos   = tos } },
                            .proto = protocol,
                            .uli_u = { .ports =
                                       { .sport = sport,
                                         .dport = dport } } };

        int err;
        if (!dst || !src) {
                err = __ip_route_output_key(rp, &fl);
                if (err)
                        return err;
                fl.fl4_dst = (*rp)->rt_dst;
                fl.fl4_src = (*rp)->rt_src;
                ip_rt_put(*rp);
                *rp = NULL;
        }
        return ip_route_output_flow(rp, &fl, sk, 0);
}

首先,构建一个搜索keyfl,在搜索要素中,来源地址/端口是不存在的。所以,当通过__ip_route_output_key进行查找时,第一次是不会命中缓存的。__ip_route_output_key将继续调用ip_route_output_slow()函数,在路由表中搜索,并返回一个合适的来源地址,并且生成一个路由缓存项。
路由查找的更多细节,我会在另一个贴子中来分析。

[Copy to clipboard][- ]CODE:/* This will initiate an outgoing connection. */
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
        struct inet_sock *inet = inet_sk(sk);
        struct tcp_sock *tp = tcp_sk(sk);
        struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
        struct rtable *rt;
        u32 daddr, nexthop;
        int tmp;
        int err;

        if (addr_len < sizeof(struct sockaddr_in))
                return -EINVAL;

        if (usin->sin_family != AF_INET)
                return -EAFNOSUPPORT;

校验地址长度和协议簇。

[Copy to clipboard][- ]CODE:nexthop = daddr = usin->sin_addr.s_addr;
将下一跳地址和目的地址的临时变量都暂时设为用户提交的地址。

[Copy to clipboard][- ]CODE:        if (inet->opt && inet->opt->srr) {
                if (!daddr)
                        return -EINVAL;
                nexthop = inet->opt->faddr;
        }

如果使用了来源地址路由,选择一个合适的下一跳地址。

[Copy to clipboard][- ]CODE:        tmp = ip_route_connect(&rt, nexthop, inet->saddr,
                               RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
                               IPPROTO_TCP,
                               inet->sport, usin->sin_port, sk);
        if (tmp < 0)
                return tmp;

        if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
                ip_rt_put(rt);
                return -ENETUNREACH;
        }

进行路由查找,并校验返回的路由的类型,TCP是不被允许使用多播和广播的。

[Copy to clipboard][- ]CODE:        if (!inet->opt || !inet->opt->srr)
                daddr = rt->rt_dst;

更新目的地址临时变量——使用路由查找后返回的值。

[Copy to clipboard][- ]CODE:        if (!inet->saddr)
                inet->saddr = rt->rt_src;
        inet->rcv_saddr = inet->saddr;

如果还没有设置源地址,和本地发送地址,则使用路由中返回的值。

[Copy to clipboard][- ]CODE:        if (tp->rx_opt.ts_recent_stamp && inet->daddr != daddr) {
                /* Reset inherited state */
                tp->rx_opt.ts_recent           = 0;
                tp->rx_opt.ts_recent_stamp = 0;
                tp->write_seq                   = 0;
        }

        if (sysctl_tcp_tw_recycle &&
            !tp->rx_opt.ts_recent_stamp && rt->rt_dst == daddr) {
                struct inet_peer *peer = rt_get_peer(rt);

                /* VJ's idea. We save last timestamp seen from
                 * the destination in peer table, when entering state TIME-WAIT
                 * and initialize rx_opt.ts_recent from it, when trying new connection.
                 */

                if (peer && peer->tcp_ts_stamp + TCP_PAWS_MSL >= xtime.tv_sec) {
                        tp->rx_opt.ts_recent_stamp = peer->tcp_ts_stamp;
                        tp->rx_opt.ts_recent = peer->tcp_ts;
                }
        }

这个更新初始状态方面的内容,还没有去分析它。

[Copy to clipboard][- ]CODE:        inet->dport = usin->sin_port;
        inet->daddr = daddr;

保存目的地址及端口。

[Copy to clipboard][- ]CODE:        tp->ext_header_len = 0;
        if (inet->opt)
                tp->ext_header_len = inet->opt->optlen;



[Copy to clipboard][- ]CODE:tp->rx_opt.mss_clamp = 536;
设置最小允许的mss值

[Copy to clipboard][- ]CODE:tcp_set_state(sk, TCP_SYN_SENT);
套接字状态被置为TCP_SYN_SENT,

[Copy to clipboard][- ]CODE:        err = tcp_v4_hash_connect(sk);
        if (err)
                goto failure;

动态选择一个本地端口,并加入hash表,与bind(2)选择端口类似。

[Copy to clipboard][- ]CODE:        err = ip_route_newports(&rt, inet->sport, inet->dport, sk);
        if (err)
                goto failure;

        /* OK, now commit destination to socket.  */
        __sk_dst_set(sk, &rt->u.dst);
        tcp_v4_setup_caps(sk, &rt->u.dst);

因为本地端口已经改变,使用新端口,重新查找路由,并用新的路由缓存项更新sk中保存的路由缓存项。

[Copy to clipboard][- ]CODE:        if (!tp->write_seq)
                tp->write_seq = secure_tcp_sequence_number(inet->saddr,
                                                           inet->daddr,
                                                           inet->sport,
                                                           usin->sin_port);

为TCP报文计算一个seq值(实际使用的值是tp->write_seq+1)。

inet->id = tp->write_seq ^ jiffies;

[Copy to clipboard][- ]CODE:        err = tcp_connect(sk);
        rt = NULL;
        if (err)
                goto failure;

        return 0;

tp_connect()函数用来根据sk中的信息,构建一个完成的syn报文,并将它发送出去。在分析tcp栈的实现时再来分析它。

根据TCP协议,接下来的问题是,
1、可能收到了服务器的应答,则要回送一个ack报文;
2、如果超时还没有应答,则使用超时重发定时器;














一、tcp栈的三次握手简述

进一步的分析,都是以tcp协议为例,因为udp要相对简单得多,分析完tcp,udp的基本已经被覆盖了。

这里主要是分析socket,但是因为它将与tcp/udp传输层交互,所以不可避免地接触到这一层面的代码,这里只是摘取其主要流程的一些代码片段,以更好地分析accept的实现过程。

当套接字进入LISTEN后,意味着服务器端已经可以接收来自客户端的请求。当一个syn包到达后,服务器认为它是一个tcp 请求报文,根据tcp
协议,TCP网络栈将会自动应答它一个syn+ack报文,并且将它放入syn_table这个hash表中,静静地等待客户端第三次握手报文的来到。一个tcp的syn报文进入tcp堆栈后,会按以下函数调用,最终进入tcp_v4_conn_request:

[Copy to clipboard][- ]CODE:tcp_v4_rcv
        ->tcp_v4_do_rcv
                ->tcp_rcv_state_process
                        ->tp->af_specific->conn_request

tcp_ipv4.c中,tcp_v4_init_sock初始化时,有

tp->af_specific = &ipv4_specific;

[Copy to clipboard][- ]CODE:struct tcp_func ipv4_specific = {
        .queue_xmit        =        ip_queue_xmit,
        .send_check        =        tcp_v4_send_check,
        .rebuild_header        =        tcp_v4_rebuild_header,
        .conn_request        =        tcp_v4_conn_request,
        .syn_recv_sock        =        tcp_v4_syn_recv_sock,
        .remember_stamp        =        tcp_v4_remember_stamp,
        .net_header_len        =        sizeof(struct iphdr),
        .setsockopt        =        ip_setsockopt,
        .getsockopt        =        ip_getsockopt,
        .addr2sockaddr        =        v4_addr2sockaddr,
        .sockaddr_len        =        sizeof(struct sockaddr_in),
};

所以af_specific->conn_request实际指向的是tcp_v4_conn_request:

[Copy to clipboard][- ]CODE:int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
        struct open_request *req;
        ……
        /* 分配一个连接请求 */
        req = tcp_openreq_alloc();
        if (!req)
                goto drop;
        ……       
        /* 根据数据包的实际要素,如来源/目的地址等,初始化它*/
        tcp_openreq_init(req, &tmp_opt, skb);

        req->af.v4_req.loc_addr = daddr;
        req->af.v4_req.rmt_addr = saddr;
        req->af.v4_req.opt = tcp_v4_save_options(sk, skb);
        req->class = &or_ipv4;               
        ……
        /* 回送一个syn+ack的二次握手报文 */
        if (tcp_v4_send_synack(sk, req, dst))
                goto drop_and_free;

        if (want_cookie) {
                ……
        } else {
                /* 将连接请求req加入连接监听表syn_table */
                tcp_v4_synq_add(sk, req);
        }
        return 0;       
}

syn_table在前面分析的时候已经反复看到了。它的作用就是记录syn请求报文,构建一个hash表。这里调用的tcp_v4_synq_add()就完成了将请求添加进该表的操作:

[Copy to clipboard][- ]CODE:static void tcp_v4_synq_add(struct sock *sk, struct open_request *req)
{
        struct tcp_sock *tp = tcp_sk(sk);
        struct tcp_listen_opt *lopt = tp->listen_opt;
        /* 计算一个hash值 */
        u32 h = tcp_v4_synq_hash(req->af.v4_req.rmt_addr, req->rmt_port, lopt->hash_rnd);

        req->expires = jiffies + TCP_TIMEOUT_INIT;
        req->retrans = 0;
        req->sk = NULL;
        /*指针移到hash链的未尾*/
        req->dl_next = lopt->syn_table[h];

        write_lock(&tp->syn_wait_lock);
        /*加入当前节点*/
        lopt->syn_table[h] = req;
        write_unlock(&tp->syn_wait_lock);

        tcp_synq_added(sk);
}

这样,所以的syn请求都被放入这个表中,留待第三次ack的到来的匹配。当第三次ack来到后,会进入下列函数:

[Copy to clipboard][- ]CODE:tcp_v4_rcv
        ->tcp_v4_do_rcv



[Copy to clipboard][- ]CODE:int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
        ……
       
        if (sk->sk_state == TCP_LISTEN) {
                struct sock *nsk = tcp_v4_hnd_req(sk, skb);
        ……
}

因为目前sk还是TCP_LISTEN状态,所以会进入tcp_v4_hnd_req:

[Copy to clipboard][- ]CODE:static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
        struct tcphdr *th = skb->h.th;
        struct iphdr *iph = skb->nh.iph;
        struct tcp_sock *tp = tcp_sk(sk);
        struct sock *nsk;
        struct open_request **prev;
        /* Find possible connection requests. */
        struct open_request *req = tcp_v4_search_req(tp, &prev, th->source,
                                                     iph->saddr, iph->daddr);
        if (req)
                return tcp_check_req(sk, skb, req, prev);
        ……
}

tcp_v4_search_req就是查找匹配syn_table表:

[Copy to clipboard][- ]CODE:static struct open_request *tcp_v4_search_req(struct tcp_sock *tp,
                                              struct open_request ***prevp,
                                              __u16 rport,
                                              __u32 raddr, __u32 laddr)
{
        struct tcp_listen_opt *lopt = tp->listen_opt;
        struct open_request *req, **prev;

        for (prev = &lopt->syn_table[tcp_v4_synq_hash(raddr, rport, lopt->hash_rnd)];
             (req = *prev) != NULL;
             prev = &req->dl_next) {
                if (req->rmt_port == rport &&
                    req->af.v4_req.rmt_addr == raddr &&
                    req->af.v4_req.loc_addr == laddr &&
                    TCP_INET_FAMILY(req->class->family)) {
                        BUG_TRAP(!req->sk);
                        *prevp = prev;
                        break;
                }
        }

        return req;
}

hash表的查找还是比较简单的,调用tcp_v4_synq_hash计算出hash值,找到hash链入口,遍历该链即可。

排除超时等意外因素,刚才加入hash表的req会被找到,这样,tcp_check_req()函数将会被继续调用:

[Copy to clipboard][- ]CODE:struct sock *tcp_check_req(struct sock *sk,struct sk_buff *skb,
                           struct open_request *req,
                           struct open_request **prev)
{
        ……
        tcp_acceptq_queue(sk, req, child);
        ……
}

req被找到,表明三次握手已经完成,连接已经成功建立,tcp_check_req最终将调用tcp_acceptq_queue(),把这个建立好的连接加入至tp->accept_queue队列,等待用户调用accept(2)来读取之。

[Copy to clipboard][- ]CODE:static inline void tcp_acceptq_queue(struct sock *sk, struct open_request *req,
                                         struct sock *child)
{
        struct tcp_sock *tp = tcp_sk(sk);

        req->sk = child;
        sk_acceptq_added(sk);

        if (!tp->accept_queue_tail) {
                tp->accept_queue = req;
        } else {
                tp->accept_queue_tail->dl_next = req;
        }
        tp->accept_queue_tail = req;
        req->dl_next = NULL;
}

二、sys_accept
如上,当listen(2)调用准备就绪的时候,服务器可以通过调用accept(2)接受或等待(注意这个“或等待”是相当的重要)连接队列中的第一个请求:

[Copy to clipboard][- ]CODE:int accept(int s, struct sockaddr * addr ,socklen_t *addrlen);
accept (2)调用,只是针对有连接模式。socket一旦经过listen(2)调用进入监听状态后,就被动地调用accept(2),接受来自客户端的连接请 求。accept(2)调用是阻塞的,也就是说如果没有连接请求到达,它会去睡觉,等到连接请求到来后(或者是超时),才会返回。同样地,操作码 SYS_ACCEPT对应的是函数sys_accept:

[Copy to clipboard][- ]CODE:asmlinkage long sys_accept(int fd, struct sockaddr __user *upeer_sockaddr, int __user *upeer_addrlen)
{
        struct socket *sock, *newsock;
        int err, len;
        char address[MAX_SOCK_ADDR];

        sock = sockfd_lookup(fd, &err);
        if (!sock)
                goto out;

        err = -ENFILE;
        if (!(newsock = sock_alloc()))
                goto out_put;

        newsock->type = sock->type;
        newsock->ops = sock->ops;

        err = security_socket_accept(sock, newsock);
        if (err)
                goto out_release;

        /*
         * We don't need try_module_get here, as the listening socket (sock)
         * has the protocol module (sock->ops->owner) held.
         */
        __module_get(newsock->ops->owner);

        err = sock->ops->accept(sock, newsock, sock->file->f_flags);
        if (err < 0)
                goto out_release;

        if (upeer_sockaddr) {
                if(newsock->ops->getname(newsock, (struct sockaddr *)address, &len, 2)<0) {
                        err = -ECONNABORTED;
                        goto out_release;
                }
                err = move_addr_to_user(address, len, upeer_sockaddr, upeer_addrlen);
                if (err < 0)
                        goto out_release;
        }

        /* File flags are not inherited via accept() unlike another OSes. */

        if ((err = sock_map_fd(newsock)) < 0)
                goto out_release;

        security_socket_post_accept(sock, newsock);

out_put:
        sockfd_put(sock);
out:
        return err;
out_release:
        sock_release(newsock);
        goto out_put;
}

代码稍长了点,逐步来分析它。

一个socket,经过listen(2)设置成server套接字后,就永远不会再与任何客户端套接字建立连接了。因为一旦它接受了一个连接请求,就会 创建出一个新的socket,新的socket用来描述新到达的连接,而原先的server套接字并无改变,并且还可以通过下一次accept(2)调用 再创建一个新的出来,就像母鸡下蛋一样,“只取蛋,不杀鸡”,server 套接字永远保持接受新的连接请求的能力。

函数先通过sockfd_lookup(),根据fd,找到对应的sock,然后通过sock_alloc分配一个新的sock。接着就调用协议簇的accept()函数:

[Copy to clipboard][- ]CODE:/*
*        Accept a pending connection. The TCP layer now gives BSD semantics.
*/

int inet_accept(struct socket *sock, struct socket *newsock, int flags)
{
        struct sock *sk1 = sock->sk;
        int err = -EINVAL;
        struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);

        if (!sk2)
                goto do_err;

        lock_sock(sk2);

        BUG_TRAP((1 << sk2->sk_state) &
                 (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE));

        sock_graft(sk2, newsock);

        newsock->state = SS_CONNECTED;
        err = 0;
        release_sock(sk2);
do_err:
        return err;
}

函数第一步工作是调用协议的accept函数,然后调用sock_graft()函数,接下来,设置新的套接字的状态为SS_CONNECTED。

[Copy to clipboard][- ]CODE:/*
*        This will accept the next outstanding connection.
*/

struct sock *tcp_accept(struct sock *sk, int flags, int *err)
{
        struct tcp_sock *tp = tcp_sk(sk);
        struct open_request *req;
        struct sock *newsk;
        int error;

        lock_sock(sk);

        /* We need to make sure that this socket is listening,
         * and that it has something pending.
         */
        error = -EINVAL;
        if (sk->sk_state != TCP_LISTEN)
                goto out;

        /* Find already established connection */
        if (!tp->accept_queue) {
                long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);

                /* If this is a non blocking socket don't sleep */
                error = -EAGAIN;
                if (!timeo)
                        goto out;

                error = wait_for_connect(sk, timeo);
                if (error)
                        goto out;
        }

        req = tp->accept_queue;
        if ((tp->accept_queue = req->dl_next) == NULL)
                tp->accept_queue_tail = NULL;

        newsk = req->sk;
        sk_acceptq_removed(sk);
        tcp_openreq_fastfree(req);
        BUG_TRAP(newsk->sk_state != TCP_SYN_RECV);
        release_sock(sk);
        return newsk;

out:
        release_sock(sk);
        *err = error;
        return NULL;
}

tcp_accept()函数,当发现tp->accept_queue准备就绪后,就直接调用

[Copy to clipboard][- ]CODE:        req = tp->accept_queue;
        if ((tp->accept_queue = req->dl_next) == NULL)
                tp->accept_queue_tail = NULL;

        newsk = req->sk;

出队,并取得相应的sk。
否则,就在获取超时时间后,调用wait_for_connect等待连接的到来。这也是说,强调“或等待”的原因所在了。

OK,继续回到inet_accept中来,当取得一个就绪的连接的sk(sk2)后,先校验其状态,再调用sock_graft()函数。

在sys_accept中,已经调用了sock_alloc,分配了一个新的socket结构(即newsock),但sock_alloc必竟不是sock_create,它并不能为newsock分配一个对应的sk。所以这个套接字并不完整。
另一方面,当一个连接到达到,根据客户端的请求,产生了一个新的sk(即sk2,但这个分配过程没有深入tcp栈去分析其实现,只分析了它对应的req入队的代码)。呵呵,将两者一关联,就OK了,这就是sock_graft的任务:

[Copy to clipboard][- ]CODE:static inline void sock_graft(struct sock *sk, struct socket *parent)
{
        write_lock_bh(&sk->sk_callback_lock);
        sk->sk_sleep = &parent->wait;
        parent->sk = sk;
        sk->sk_socket = parent;
        write_unlock_bh(&sk->sk_callback_lock);
}

这样,一对一的联系就建立起来了。这个为accept分配的新的socket也大功告成了。接下来将其状态切换为SS_CONNECTED,表示已连接就绪,可以来读取数据了——如果有的话。

顺便提一下,新的sk的分配,是在:
]tcp_v4_rcv
        ->tcp_v4_do_rcv
                     ->tcp_check_req
                              ->tp->af_specific->syn_recv_sock(sk, skb, req, NULL);
即tcp_v4_syn_recv_sock函数,其又调用tcp_create_openreq_child()来分配的。

[Copy to clipboard][- ]CODE:struct sock *tcp_create_openreq_child(struct sock *sk, struct open_request *req, struct sk_buff *skb)
{
        /* allocate the newsk from the same slab of the master sock,
         * if not, at sk_free time we'll try to free it from the wrong
         * slabcache (i.e. is it TCPv4 or v6?), this is handled thru sk->sk_prot -acme */
        struct sock *newsk = sk_alloc(PF_INET, GFP_ATOMIC, sk->sk_prot, 0);

        if(newsk != NULL) {
                          ……
                         memcpy(newsk, sk, sizeof(struct tcp_sock));
                         newsk->sk_state = TCP_SYN_RECV;
                          ……
}

等到分析tcp栈的实现的时候,再来仔细分析它。但是这里新的sk的有限状态机被切换至了 TCP_SYN_RECV(按我的想法,似乎应进入establshed才对呀,是不是哪儿看漏了,只有看了后头的代码再来印证了)

回到sys_accept中来,如果调用者要求返回各户端的地址,则调用新的sk的getname函数指针,也就是inet_getname:

[Copy to clipboard][- ]CODE:/*
*        This does both peername and sockname.
*/
int inet_getname(struct socket *sock, struct sockaddr *uaddr,
                        int *uaddr_len, int peer)
{
        struct sock *sk                = sock->sk;
        struct inet_sock *inet        = inet_sk(sk);
        struct sockaddr_in *sin        = (struct sockaddr_in *)uaddr;

        sin->sin_family = AF_INET;
        if (peer) {
                if (!inet->dport ||
                    (((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_SYN_SENT)) &&
                     peer == 1))
                        return -ENOTCONN;
                sin->sin_port = inet->dport;
                sin->sin_addr.s_addr = inet->daddr;
        } else {
                __u32 addr = inet->rcv_saddr;
                if (!addr)
                        addr = inet->saddr;
                sin->sin_port = inet->sport;
                sin->sin_addr.s_addr = addr;
        }
        memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
        *uaddr_len = sizeof(*sin);
        return 0;
}

函数的工作是构建珍上struct sockaddr_in 结构出来,接着在sys_accept中,调用move_addr_to_user()函数来拷贝至用户空间:

[Copy to clipboard][- ]CODE:int move_addr_to_user(void *kaddr, int klen, void __user *uaddr, int __user *ulen)
{
        int err;
        int len;

        if((err=get_user(len, ulen)))
                return err;
        if(len>klen)
                len=klen;
        if(len<0 || len> MAX_SOCK_ADDR)
                return -EINVAL;
        if(len)
        {
                if(copy_to_user(uaddr,kaddr,len))
                        return -EFAULT;
        }
        /*
         *        "fromlen shall refer to the value before truncation.."
         *                        1003.1g
         */
        return __put_user(klen, ulen);
}

也就是调用copy_to_user的过程了。

sys_accept的最后一步工作,是将新的socket结构,与文件系统挂钩:

[Copy to clipboard][- ]CODE:        if ((err = sock_map_fd(newsock)) < 0)
                goto out_release;

函数sock_map_fd在创建socket中已经见过了。

小结:
accept有几件事情要做
1、要accept,需要三次握手完成,连接请求入tp->accept_queue队列(新为客户端分析的sk,也在其中),其才能出队;
2、为accept分配一个sokcet结构,并将其与新的sk关联;
3、如果调用时,需要获取客户端地址,即第二个参数不为NULL,则从新的sk中,取得其想的葫芦;
4、将新的socket结构与文件系统挂钩;



转自:http://blog.163.com/s_xli1/