このブログではネットワークに関する比較的新しい技術について触れてきたが、たまには古きを温めるのも良いだろうということで読んでみた。Linuxカーネルは今後も長きにわたって使われるはずで、カンペキな理解でなくとも、取っ掛かりだけでも掴んでいる意味は大きいと思う。この本は1,000頁超えで、1~7部から構成されているので、一気に読むのはモチベーション維持が難しいと思う。この記事ではとりあえず現時点で読んだところまでをまとめたい。カーネルバージョン 2.6.39 のソースコードを手元に置いて、読み進めていった。ビルド方法などは前回の記事 1 のとおり。

1部

ネットワークに関する重要なデータ構造として struct sk_buffstruct net_device がある。まずはこの2つのデータ構造を掴むことが肝要だと思う。struct sk_buff は(フラグメンテーション云々の話を抜きにすると)1つのパケットに対応する。しばしばそのインスタンスは skb という名前が付けられる。skb->data が処理を担当しているネットワークレイヤのヘッダを指している。例えば、L2の処理を行っている際にはskb->data はL2ヘッダ の先頭を指している。処理の進行に伴って、このポインタは移動していく。実データの前後に余白が設けられている。

+------------+                           skb->mac   skb->nh
|            |                              |         |
| head----------->  +------------+          |         |
|            |      | headroom   |          v         v
| data----------->  +------------+          +---------+---------+---------+---
|            |      |            |          | L2      | L3      | L4      |
| tail       |      | Data       |          | header  | header  | header  | ...
|     |      |      |            |          +---------+---------+---------+---
| end |      |      |            |          ^         ^
|  |  +---------->  +------------+          |         |
|  |         |      | tailroom   |          |         |
|  +------------->  +------------+          +---------+
|            |                               skb->data
+------------+
struct sk_buff

この構造体にどんなメンバがいるか見ていく。users が参照カウンタに対応していて、sk_getkfree_skbで操作できる。mac_header など各レイヤに対応するポインタもある。cbはコントロールバッファの略で、48バイトの領域を各レイヤの中でプライベート(他のレイヤを意識せず)に使える。struct sk_buffは双方向リストで管理されていて、リスト全体は struct sk_buff_head に対応する。デバッガを使って、中身を見ていく。送信を担当する関数にアタッチしてみると、struct sk_buff内部に保持されたIPヘッダ の中身を見ることができる。

(gdb) list dev_hard_start_xmit
2086    int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
2087                            struct netdev_queue *txq)
2088    {
2089            const struct net_device_ops *ops = dev->netdev_ops;
2090            int rc = NETDEV_TX_OK;
2091
2092            if (likely(!skb->next)) {
(gdb) break dev_hard_start_xmit
(gdb) continue
Breakpoint 2 at 0xffffffff8140e036: file net/core/dev.c, line 2092.
(gdb) print *((struct iphdr *)(skb->head + skb->network_header))
$3 = {ihl = 5 '\005', version = 4 '\004', tos = 0 '\000', tot_len = 18433,
  id = 0, frag_off = 0, ttl = 64 '@', protocol = 17 '\021', check = 42617,
  saddr = 0, daddr = 4294967295}

続いて、struct net_deviceについて見ていく。これは、仮想・物理を問わずネットワークインターフェイスごとに1つずつ生成されるデータである。グローバル変数dev_baseでリスト管理されている。デバイス名、IRQ番号、各種フラグ(flagsgflagspriv_flags)が含まれる。もし仮想インターフェイスの場合には、masterフィールドを辿ると元デバイスを探すことができる。インターフェイスはしばしば名前やインデックス?から検索されることがあるので、dev_index_headdev_name_headで提供されるハッシュテーブルが存在する。先ほどと同じく、送信を担当する関数にデバッガをアタッチして、struct net_deviceの中身を見ていく。

(gdb) print init_net->dev_base_head
$8 = {next = 0xffff88001d89f080, prev = 0xffff88001ce5f080}
(gdb) print init_net->dev_name_head
$9 = (struct hlist_head *) 0xffff88001d878800
(gdb) print init_net->dev_index_head
$10 = (struct hlist_head *) 0xffff88001d89f800
(gdb) print *dev
$11 = {name = "eth0", '\000' <repeats 11 times>,
       pm_qos_req = {list = {prio = 0, prio_list = { ...

話は変わり、ユーザ・カーネル間のインターフェイス周りへと移る。ここには歴史的に多くのインターフェイスがある。まとめると、こんな感じかな。

名前 内容
procfs(/proc) 普通はリードオンリー。ネットワーク関連は/proc/netに纏まっている。proc_net_fops_createで登録できる。
sysctl(/proc/sys) sysctlコマンドから使える。実際のカーネル内変数に対応する。register_sysctl_tableで登録できる。
sysfs(/sys) カーネル2.6でprocfsやsysctlの内容を整理し直したもの。
ioctl ifconfig、ethtool、mii-tools から使われる。
netlink 最近の仕組みでソケットAPIで提供される。iproute2から使われる。唯一 カーネルからユーザの方向 に通知を送れる。

ifconfigコマンドからioctlを発行すると、SIOCGIFADDRなどリクエストの内容に応じて、sock_ioctl()を経由してdevinet_ioctl()が呼ばれる。デバッガを仕掛けてみると、確かに呼ばれていることがわかる。ちなみに、ethtoolコマンドから呼ばれる関数は、ドライバコードで定義されたstruct ethtool_ops内コールバックのようだ。

(gdb) list devinet_ioctl
685     int devinet_ioctl(struct net *net, unsigned int cmd, void __user *arg)
686     {
687             struct ifreq ifr;
688             struct sockaddr_in sin_orig;
689             struct sockaddr_in *sin = (struct sockaddr_in *)&ifr.ifr_addr;
690             struct in_device *in_dev;
(gdb) break devinet_ioctl
Breakpoint 3 at 0xffffffff81475264: file net/ipv4/devinet.c, line 702.
(gdb) continue
(gdb) bt
#0  devinet_ioctl (net=0xffffffff81f8b040 <init_net>, cmd=35093, arg=0x7fff25324a10) at net/ipv4/devinet.c:702
#1  0xffffffff814769d8 in inet_ioctl (sock=<optimized out>, cmd=<optimized out>, arg=<optimized out>)
    at net/ipv4/af_inet.c:870
#2  0xffffffff813f7cc0 in sock_do_ioctl (net=0xffffffff81f8b040 <init_net>, sock=<optimized out>, cmd=35093,
    arg=140733817440784) at net/socket.c:945
#3  0xffffffff813f8119 in sock_ioctl (file=<optimized out>, cmd=35093, arg=<optimized out>) at net/socket.c:1030
#4  0xffffffff8116cd8c in vfs_ioctl (arg=<optimized out>, cmd=<optimized out>, filp=0xffff88001dbea200)
    at fs/ioctl.c:43
#5  do_vfs_ioctl (filp=0xffff88001dbea200, fd=3, cmd=<optimized out>, arg=<optimized out>) at fs/ioctl.c:598
#6  0xffffffff8116d0e1 in sys_ioctl (fd=3, cmd=35093, arg=140733817440784) at fs/ioctl.c:618
#7  0xffffffff814dd5c2 in system_call () at arch/x86/kernel/entry_64.S:487
#8  0x000000000049e417 in ?? ()
#9  0x0000000000000000 in ?? ()
(gdb) p /x cmd
$14 = 0x8915 # SIOGIFADDR 0x8915 に対応
(gdb) p ifr.ifr_ifrn.ifrn_name
$15 = "\002\000\000\000\000\000\000\000\001\000\000\000\000\000\000"

2部

カーネルの中にはいくつかサブシステムがあり、それらは相互に依存しているので、あるサブシステムでイベントが発生・検知したとすると、それを別のサブシステムに通知したくなる。これを通知チェインの仕組みで実現している。例えば、リンクダウンが発生した時に、ルーティングテーブルからエントリを削除する場合など。xxx_chainxxx_notifler_chainxxx_notifiler_list という関数ポインタのリストがあるので、そこに追加するだけというシンプルな仕組みになっている。これらのリストには、notifier_chain_register で関数ポインタを登録する。実際には、register_inetaddr_notifierregister_netdevice_notifier のようなラッパーがあることが多い。呼び出しは notifler_call_chain で行われる。リストに登録された関数は、この関数の呼び出し元コンテキストで実行されるので注意する。例えば、inetaddr_chainnetdev_chainのようなネットワーク関連の通知チェインには別のサブシステムの関数が登録されることがあれば、反対にネットワーク関連の関数が reboot_notifier_list に登録されることもある。

システム全体からみたブート周りをみていく。ブートされるとstart_kernelが呼ばれ、その中で init カーネルスレッドが開始される。 do_initcallsの中では .initcallN.init を順に実行していく。ここで .init.setup がカーネルパラメータに対応し、device_initcall が静的リンクされたデバイスドライバの初期化に対応する。一般的なパラメータは __setup マクロ、初期段階で必要なパラメータは early_param マクロを使って定義される。また、カーネルモジュールのパラメータは module_param マクロで定義でき、/sys/module/モジュール名/parameters/パラメータ名 に展開される。

(gdb) list do_initcalls
695     static void __init do_initcalls(void)
696     {
697             initcall_t *fn;
698
699             for (fn = __early_initcall_end; fn < __initcall_end; fn++)
700                     do_one_initcall(*fn);
701     }
(gdb) b do_initcalls
Breakpoint 2 at 0xffffffff81c23690: file init/main.c, line 700.
(gdb) continue
(gdb) bt
#0  do_initcalls () at init/main.c:700
#1  do_basic_setup () at init/main.c:718
#2  0xffffffff81c23889 in kernel_init (unused=<optimized out>) at init/main.c:801
#3  0xffffffff814de704 in kernel_thread_helper () at arch/x86/kernel/entry_64.S:1161
#4  0x0000000000000000 in ?? ()
                                                    Macro

    _init_begin  ----->  +---------------------+
                         | .init.text          |   __init
                         |                     |
                         +---------------------+
                         | .init.data          |   __initdata
                         |                     |
   _setup_start  ----->  +---------------------+
                         | .init.setup         |  __setup_param
                         |                     |
                         |                     |
__initcall_start ----->  +---------------------+
                         | .initcall1.init     |  core_initcall
                         |                     |
                         +---------------------+
                         | .initcall2.init     |  postcore_initcall
                         |                     |
                         +---------------------+
                         | ...                 |  ...
                         |                     |
                         |                     |
                         |                     |
                         |                     |
                         +---------------------+
                         | .initcall6.init     |  device_initcall
                         |                     |
                         +---------------------+

デバイスドライバはPCI層とどのように連携するのか。ここでは以下の3つのデータ構造が重要になる。どれもデバイスドライバ内で初期化・登録される。

// PCIデバイスの特定の機種に対応
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 */
};

// PCIデバイスに対応
struct pci_dev {
  struct list_head bus_list;  /* node in per-bus list */
  struct pci_bus  *bus;   /* bus this device is on */
  struct pci_bus  *subordinate; /* bus this device bridges to */

  void    *sysdata; /* hook for sys-specific extension */
  struct proc_dir_entry *procent; /* device entry in /proc/bus/pci */
  struct pci_slot *slot;    /* Physical slot this device is in */

  unsigned int  devfn;    /* encoded device & function index */
  unsigned short  vendor;
  unsigned short  device;
  unsigned short  subsystem_vendor;
  unsigned short  subsystem_device;
  unsigned int  class;    /* 3 bytes: (base,sub,prog-if) */
  u8    revision; /* PCI revision, low byte of class word */
  u8    hdr_type; /* PCI header type ('multi' flag masked out) */
  u8    pcie_cap; /* PCI-E capability offset */
  u8    pcie_type;  /* PCI-E device/port type */
  u8    rom_base_reg; /* which config register controls the ROM */
  u8    pin;      /* which interrupt pin this device uses */

  struct pci_driver *driver;  /* which driver has allocated this device */
  ...
};

// PCIデバイスドライバに対応
struct pci_driver {
  struct list_head node;
  const char *name;
  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  (*suspend_late) (struct pci_dev *dev, pm_message_t state);
  int  (*resume_early) (struct pci_dev *dev);
  int  (*resume) (struct pci_dev *dev);                 /* Device woken up */
  void (*shutdown) (struct pci_dev *dev);
  struct pci_error_handlers *err_handler;
  struct device_driver  driver;
  struct pci_dynids dynids;
};

例としてIntel e100ドライバの例も載せておく。

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

module_init(e100_init_module);

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
  /* Power Management hooks */
  .suspend =      e100_suspend,
  .resume =       e100_resume,
#endif
  .shutdown =     e100_shutdown,
  .err_handler = &e100_err_handler,
};

#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 DEFINE_PCI_DEVICE_TABLE(e100_id_table) = {
  INTEL_8255X_ETHERNET_DEVICE(0x1029, 0),
  INTEL_8255X_ETHERNET_DEVICE(0x1030, 0),
  INTEL_8255X_ETHERNET_DEVICE(0x1031, 3),
  INTEL_8255X_ETHERNET_DEVICE(0x1032, 3),
  ...
};

xxx_probe の中でデバイスが検出されると、struct net_device 用のメモリが割り当てられ、register_netdev によって dev_base へと登録される。struct net_device にはパラメータが大量にあるので、初期化箇所がether_setupやデバイスドライバのprobe関数などに分かれている。

(gdb) list ether_setup
334     void ether_setup(struct net_device *dev)
335     {
336             dev->header_ops         = &eth_header_ops;
337             dev->type               = ARPHRD_ETHER;
338             dev->hard_header_len    = ETH_HLEN;
339             dev->mtu                = ETH_DATA_LEN;
(gdb) break ether_setup
Breakpoint 2 at 0xffffffff8142a0a9: file net/ethernet/eth.c, line 336.
(gdb) continue
Continuing.

(gdb) bt
#0  ether_setup (dev=0xffff88001ce61000) at net/ethernet/eth.c:336
#1  0xffffffff81412215 in alloc_netdev_mqs (sizeof_priv=<optimized out>, name=0xffffffff817f4daa "eth%d",
    setup=0xffffffff8142a0a0 <ether_setup>, txqs=1, rxqs=1) at net/core/dev.c:5824
#2  0xffffffff8142a091 in alloc_etherdev_mqs (sizeof_priv=<optimized out>, txqs=<optimized out>,
    rxqs=<optimized out>) at net/ethernet/eth.c:367
#3  0xffffffff813686a1 in virtnet_probe (vdev=0xffff88001cdc7c00) at drivers/net/virtio_net.c:904
#4  0xffffffff812cd903 in virtio_dev_probe (_d=0xffff88001cdc7c08) at drivers/virtio/virtio.c:139
#5  0xffffffff8131d537 in really_probe (dev=0xffff88001cdc7c08, drv=0xffffffff81a95100 <virtio_net_driver>)
    at drivers/base/dd.c:129
#6  0xffffffff8131d73e in driver_probe_device (drv=0xffffffff81a95100 <virtio_net_driver>,
    dev=0xffff88001cdc7c08) at drivers/base/dd.c:212
#7  0xffffffff8131d84b in __driver_attach (dev=0xffff88001cdc7c08, data=0xffffffff81a95100 <virtio_net_driver>)
    at drivers/base/dd.c:286

デバイスドライバは割り込みハンドラの初期化も担当している。これは request_irq で実現している。ここで、SA_SHIRQを有効にすると、その1つのIRQ番号を複数の割り込みハンドラで共有することができる。

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
       irq_handler_t thread_fn, unsigned long irqflags,
       const char *devname, void *dev_id);

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
      const char *name, void *dev)
{
  return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

struct irqaction {
  irq_handler_t handler;
  unsigned long flags;
  void *dev_id;
  struct irqaction *next;
  int irq;
  irq_handler_t thread_fn;
  struct task_struct *thread;
  unsigned long thread_flags;
  unsigned long thread_mask;
  const char *name;
  struct proc_dir_entry *dir;
} ____cacheline_internodealigned_in_smp;

struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
  [0 ... NR_IRQS-1] = {
    .handle_irq = handle_bad_irq,
    .depth    = 1,
    .lock   = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
  }
};

デバイスが接続されたとき、どのドライバが選ばれるのか。実はこの仕組みの中で、カーネル・ユーザ・カーネルというように、一旦ユーザを介するのが面白い。例えば、modprobe eth0 を実行すると、/etc/modprobe.confに記載された alias eth0 3c59x をもとに、デバイスドライバ 3c59x が読み込まれる。カーネル関数としては request_modulecall_usermodehelper が対応する。

NICはリンク状態をどのように検知しているのか。ハードウェアがキャリアやシグナルの変化を検知すると、通知やConfigration Registerの変更を行う。その後、デバイスドライバがそれを見つけ、linkwatch_fire_event を呼び出してイベントを登録する。このイベントは、keventd_wqカーネルスレッド内の linkwatch_event によって実行される。linkwatch_event は、struct net_device 内の state変更と通知を担当する。