Сетвой стек в Linux: введение (SKBs)

Сетевой стек Linux в настоящее время является рабочей лошадкой современной сети Internet (и не только её). Действительно, его возможности в настоящее время очень широкие. Достаточно посмотреть в директорию net ядра:

denis@denis-desktop:~/syssrc/linux-2.6/net$ ls
802        can           econet      key        netrom   sctp          wireless
8021q      ceph          ethernet    l2tp       nonet.c  socket.c      x25
9p         compat.c      ieee802154  lapb       packet   sunrpc        xfrm
appletalk  core          ipv4        llc        phonet   sysctl_net.c
atm        dcb           ipv6        mac80211   rds      tipc
ax25       dccp          ipx         Makefile   rfkill   TUNABLE
bluetooth  decnet        irda        netfilter  rose     unix
bridge     dns_resolver  iucv        netlabel   rxrpc    wanrouter
caif       dsa           Kconfig     netlink    sched    wimax

Почти все имена говорят сами за себя.
Сетевая подсистема является достаточно динамично развивающейся, о чем говорит архив рассылки netdev: http://marc.info/?l=linux-netdev

Однако в основе стека лежит совсем небольшое количество объектов (я не про ООП :-) ):
структура struct sk_buff, структура struct net_device, механизм SoftIRQ и конечно, его разработчики :)
(я пока опущу protocol handler и socket)

Рабочей лошадкой всего стека является сетевой пакет. Точка. То, что представляет сетевой пакет я ядре является структурой struct sk_buff (в *BSD это называется mbuf). (кстати, судя по комментариям в файле include/linux/skbuff.h авторами-основателями были Alan Cox и Florian La Roche). Когда пакет передается от пользовательского приложения ядру через интерфейс сокетов, то ядро создает для него socket kernel buffer (skb). skb "путешествует" между различными уровнями сетевого стека пока не достигнет драйвера устройства. skb соответсвует каждому принимаемому/отправляемому пакету.

Cтруктура является относительно большой и я не буду приводить ее полное содержание. Важно отметить что skb (а именно так нызывают разработчики Linux сетевой пакет) является на самом деле "дескриптором" пакета, т.е. сами данные находятся в отдельной памяти, и место для них выделяется с помощью динамического аллокатора ядра (то что является надстройкой для low level page allocator):

 170 struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
 171                            int fclone, int node)
 172 {
...
 186        size = SKB_DATA_ALIGN(size);
 187        data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info),
 188                        gfp_mask, node);
...
 233 }
 234 EXPORT_SYMBOL(__alloc_skb);

Если заметить, вместе с требуемым размеров, дополнительно выделяется память для структуры struct skb_shared_info. Эта структура содержит информацию о так называемых нелинейных данных. Когда IP-датаграмма фрагментированна на пакеты, полe frag_list этой структуры представляет собой список фрагментов (т.е. структур struct sk_buff). Еще одна особенность этой структуры - поддержка * not physically-continuous* данных (или, инными словами, scatter-gather DMA). В этом случае 1) данне _распределены_ по страницам памяти (именно страница является минимальным блоком) и 2) драйвер устройства поддерживает эту функицональную возможность (устанавливая при этом флаг NETIF_F_SG и говоря сетевому стеку: 'Привет, я поддерживаю scatter-gather'). Поля nr_frags и frags[] служат именно для этого.
/* This data is invariant across clones and lives at
 * the end of the header data, ie. at skb->end.
 */
struct skb_shared_info {
   unsigned short  nr_frags;
...
    struct sk_buff  *frag_list;
...
   skb_frag_t  frags[MAX_SKB_FRAGS];
}

Особенностью skb является то, что память для данных выделяется один раз. Если представить себе горизонтально вытянутый прямоугольник, то данные в нем будут начинаться где-то в середине c началом в skb->head и концом в skb->tail. Начало области зарезервировано для заголовков различных уровней, и начинается с skb->head. Конец области данных skb содержится в skb->end. При этом область между ->head и ->data называется headroom, а область между ->tail и ->end - tailroom. Linux предоставляет множество функций для манипулирования данными указателями (мы _ничего не копируем_) в файлe include/linux/skbuff.h. Как пример, можно рассмотреть skb_reserve():

1251 */
1252 static inline void skb_reserve(struct sk_buff *skb, int len)
1253 {
1254        skb->data += len;
1255        skb->tail += len;
1256 }

Эту inline функию часто используют как:

denis@denis-desktop:~/syssrc/linux-2.6$ grep -nr "skb_reserve" drivers/net/
drivers/net/plip.c:638:		skb_reserve(rcv->skb, 2);	/* Align IP on 16 byte boundaries */
drivers/net/ucc_geth.c:223:	skb_reserve(skb,
drivers/net/e1000e/netdev.c:694:	unsigned int bufsz = 256 - 16 /* for skb_reserve */;
drivers/net/e1000e/ethtool.c:1229:		skb_reserve(skb, NET_IP_ALIGN);
drivers/net/pcnet32.c:599:		skb_reserve(rx_skbuff, NET_IP_ALIGN);
drivers/net/pcnet32.c:1173:			skb_reserve(newskb, NET_IP_ALIGN);
drivers/net/pcnet32.c:1199:		skb_reserve(skb, NET_IP_ALIGN);
drivers/net/pcnet32.c:2300:			skb_reserve(rx_skbuff, NET_IP_ALIGN);
drivers/net/3c59x.c:1767:			skb_reserve(skb, NET_IP_ALIGN);	/* Align IP on 16 byte boundaries */
drivers/net/3c59x.c:2507:				skb_reserve(skb, 2);	/* Align IP on 16 byte boundaries */
drivers/net/3c59x.c:2582:				skb_reserve(skb, 2);	/* Align IP on 16 byte boundaries */
...

Идея тут заключается в резервировании n байт (например, 2) для того чтобы IP заголовок входящего пакета был выровнен по границе 16 байт.
Архитектуры для которых требуется выровненный доступ к данным обычно используют четырех байтную границу.
Т.е. если мы получили на ethernet интерфейс пакет, то ethernet frame header занимает 14 байт, затем 2 байта мы резервируем. В результате IP header выровнен на 16 байт (n * 4 + 2 делится на 4 :-).

 
 118 struct ethhdr {
 119        unsigned char   h_dest[ETH_ALEN];       /* destination eth addr */
 120        unsigned char   h_source[ETH_ALEN];     /* source ether addr    */
 121        __be16          h_proto;                /* packet type ID field */
 122 } __attribute__((packed));

Мы резервируем область для заголовков в headroom которые _будут добавлены позже_. Т.к. skb->head всё время указывает на начало выделенного блока данных, то область в headroom определяется как skb->data - skb->head.

1228static inline unsigned int skb_headroom(const struct sk_buff *skb)
1229 {
1230        return skb->data - skb->head;
1231 }
 
1239static inline int skb_tailroom(const struct sk_buff *skb)
1240 {
1241        return skb_is_nonlinear(skb) ? 0 : skb->end - skb->tail;
1242 }

David S. Miller хорошо отобразил эти функции тут: http://vger.kernel.org/~davem/skb_data.html

skb содержит в себе указатели на заголовки различных уровней, в них входят:

    sk_buff_data_t      transport_header;
    sk_buff_data_t      network_header;
    sk_buff_data_t      ;
...
и функции доступа к этип полям:
 
static inline unsigned char *skb_transport_header(const struct sk_buff *skb);
static inline unsigned char *skb_network_header(const struct sk_buff *skb);
static inline unsigned char *skb_mac_header(const struct sk_buff *skb);

*) transport_header - транспортный уровень (Level 4), который раньше назывался h.
*) network_header - сетевой уровень (Level 3), раньше известный как nh.
*) mac_header - mac уровень (уровень доступа к среде, Level 2), ранее mac.

Я ядре очень многие части могут быть заинтересованы в skb, поэтому очень часто с одним и тем же пакетом работают несколько клиентов.
С одной стороны, на один и тот же skb(а не данные) могут ссылаться несколько указателей и за это отвечает счетчик skb->users. Очень важный момент заключается в том,
что когда skb->users > 1 то такой буфер является read-only! В противном случае (при записи) данные в skb могут быть запросто повреждены (на лицо состояние race condition). Как только счетчик принимает значение 0, такой буфер может быть освобожден.
С другой стороны, skb могут быть клонированы, т.е. когда на одни и те же данные (выделенные с помощью kmalloc) ссылаются несколько "дескрипторов" пакета. Другими словами когда каждому нужно внести свои изменения в skb (который как мы помним является "дескриптором" для пакета в ядре), при этом оставив данные в "первозданном" виде. В таком случае устанавливается флаг skb->cloned.
Стоит упомянуть что у области данных пакета тоже есть свой счетчик, который хранится в выше упомянутой структуре skb_shared_info и называется dataref. Очень легко понять что при клонировании буфера счетчик dataref увеличивается.

Список skb называется очередью что и не удивительно.

 114 struct sk_buff_head {
 115        /* These two members must be first. */
 116        struct sk_buff  *next;
 117        struct sk_buff  *prev;
 118
 119        __u32           qlen;
 120        spinlock_t      lock;
 121 };

Комментарий в начале структуры говорит о том, что первые два члена у struct sk_buff_head и struct sk_buff одинаковые. Это позволяет использовать взаимозаменяемые указатели при обработке очереди.

Очереди используется, например, при обработке входящих пакетов, при чем у каждого процессора в системе она своя (при использовании механизма обработки пакетов NAPI очередь своя уже у каждого устройства, а вернее драйвера устройства). Функция netif_rx как раз и занимается тем, что добавляет приходящие пакеты в очередь. Пока пакет не обработан, он хранится в очереди и ждет своего момента.

Пока что на этом закончу, но это, конечно же, совсем не конец истории. В следующей части я рассмотрю интерфейс net_device: основу, которая является мостиком между драйвером устройства, с одной стороны, и сетевым стеком, с другой.

---
Signed-off-by: Denis Kirjanov