このブログではネットワークに関する比較的新しい技術について触れてきたが、たまには古きを温めるのも良いだろうということで読んでみた。Linuxカーネルは今後も長きにわたって使われるはずで、カンペキな理解でなくとも、取っ掛かりだけでも掴んでいる意味は大きいと思う。この本は1,000頁超えで、1~7部から構成されているので、一気に読むのはモチベーション維持が難しいと思う。この記事ではとりあえず現時点で読んだところまでをまとめたい。カーネルバージョン 2.6.39 のソースコードを手元に置いて、読み進めていった。ビルド方法などは前回の記事 1 のとおり。
1部
ネットワークに関する重要なデータ構造として struct sk_buff
と struct 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_get
やkfree_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番号、各種フラグ(flags
、gflags
、priv_flags
)が含まれる。もし仮想インターフェイスの場合には、master
フィールドを辿ると元デバイスを探すことができる。インターフェイスはしばしば名前やインデックス?から検索されることがあるので、dev_index_head
やdev_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_chain
、xxx_notifler_chain
、xxx_notifiler_list
という関数ポインタのリストがあるので、そこに追加するだけというシンプルな仕組みになっている。これらのリストには、notifier_chain_register
で関数ポインタを登録する。実際には、register_inetaddr_notifier
やregister_netdevice_notifier
のようなラッパーがあることが多い。呼び出しは notifler_call_chain
で行われる。リストに登録された関数は、この関数の呼び出し元コンテキストで実行されるので注意する。例えば、inetaddr_chain
やnetdev_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 = ð_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_module
や call_usermodehelper
が対応する。
NICはリンク状態をどのように検知しているのか。ハードウェアがキャリアやシグナルの変化を検知すると、通知やConfigration Registerの変更を行う。その後、デバイスドライバがそれを見つけ、linkwatch_fire_event
を呼び出してイベントを登録する。このイベントは、keventd_wqカーネルスレッド内の linkwatch_event
によって実行される。linkwatch_event
は、struct net_device
内の state変更と通知を担当する。