🤖

udevがenp1s0を名付けるまで

に公開

udevが動作するOSではネットワークインターフェース名にenp1s0といった名前が割り当てられることがあります
この記事ではこのインターフェース名が何者なのか、そしてどこからやってきたのかを解説します

結論

  1. Linuxカーネルは起動後にPCIデバイスを走査してカーネル内部のデバイスドライバがnet_deviceを初期化する。このとき、net_deviceの名前は"eth%d"形式で初期化される
  2. udevはユーザープロセス起動後にNetlink APIを通じてnet_deviceの名前をenp1s0に変更する
  3. (おまけ)ユーザーはNetlink APIを使用してnet_deviceの情報からネットワークインターフェース名enp1s0を取得する

環境

$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.1 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

$ cat /proc/version
Linux version 6.8.0-63-generic (buildd@lcy02-amd64-047) (x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, GNU ld (GNU Binutils for Ubuntu) 2.42) #66-Ubuntu SMP PREEMPT_DYNAMIC Fri Jun 13 20:25:30 UTC 2025

$ ip -V
ip utility, iproute2-6.1.0, libbpf 1.3.0
$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp86s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether 48:21:0b:37:ae:a1 brd ff:ff:ff:ff:ff:ff
$ lspci -nn -k
...
56:00.0 Ethernet controller [0200]: Intel Corporation Ethernet Controller I225-V [8086:15f3] (rev 03)
        Subsystem: Intel Corporation Ethernet Controller I225-V [8086:3024]
        Kernel driver in use: igc
        Kernel modules: igc
$ systemctl --version
systemd 255 (255.4-1ubuntu8.10)
+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT +QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK -XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified

ipコマンドはnet_deviceを見ている


まず最初に私たちが何気なく参照しているインターフェース名がどこに存在しているのかを確認しておきます
人間が直接インターフェース名を確認する際に最も多く使用されるのはipコマンドかと思います
そのためまずはipコマンドが何を参照しているのかについて取り上げたいと思います

ipコマンドはiproute2パッケージの一部でありkernel.orgでホスティングされているLinux用のネットワークユーティリティツールです
このコマンドはC言語で実装されており内部的にはLinuxカーネルと通信してデバイスの情報を取得しています

まずはipコマンドがLinuxカーネルと通信していることを確認しましょう
ユーザープログラムがカーネルと通信している様子はstraceコマンドで追跡することができます
以下はstrace ip linkの一部抜粋です

...
socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_ROUTE) = 3
sendto(3, [{nlmsg_len=40, nlmsg_type=RTM_GETLINK, nlmsg_flags=NLM_F_REQUEST|NLM_F_DUMP, nlmsg_seq=1755362708, nlmsg_pid=0}, {ifi_family=AF_PACKET, ifi_type=ARPHRD_NETROM, ifi_index=0, ifi_flags=0, ifi_change=0}, [{nla_len=8, nla_type=IFLA_EXT_MASK}, RTEXT_FILTER_VF]], 40, 0, NULL, 0) = 40
recvmsg(3, {msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=12, msg_iov=[{iov_base=NULL, iov_len=0}], msg_iovlen=1, msg_controllen=0, msg_flags=MSG_TRUNC}, MSG_PEEK|MSG_TRUNC) = 2924
recvmsg(3, {msg_name={sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, msg_namelen=12, msg_iov=[{iov_base=[[{nlmsg_len=1444, nlmsg_type=RTM_NEWLINK, nlmsg_flags=NLM_F_MULTI, nlmsg_seq=1755362708, nlmsg_pid=36831}, {ifi_family=AF_UNSPEC, ifi_type=ARPHRD_LOOPBACK, ifi_index=if_nametoindex("lo"), ifi_flags=IFF_UP|IFF_LOOPBACK|IFF_RUNNING|IFF_LOWER_UP, ifi_change=0}, [[{nla_len=7, nla_type=IFLA_IFNAME}, "lo"], [{nla_len=8, nla_type=IFLA_TXQLEN}, 1000], ...
...

socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_ROUTE) = 3でNETLINK_ROUTEソケットを生成しています
このソケットでカーネルからルーティング情報やネットワークデバイスの情報を取得することができます
sendtoでカーネルに対してリクエストを送信してrecvmsgでカーネルから実際のデバイス情報を取得しています
一回目のrecvmsgは受信するメッセージのサイズを調べるためのダミー呼び出しで二回目のrecvmsgがデバイス情報の本体です
わかりにくいですが[[{nla_len=7, nla_type=IFLA_IFNAME}, "lo"]の部分がカーネルから受信したループバックデバイスの名前です
イーサネットデバイスであれば[[{nla_len=11, nla_type=IFLA_IFNAME}, "enp1s0"]などと表示されます

もう少し深堀りしましょう
カーネルとNetlink APIで通信するためにはまずはリクエストを送信する必要があります
このリクエストはNelinkヘッダー + メッセージの形式をしています
Netlinkヘッダーの構造は以下です

struct nlmsghdr {
    __u32 nlmsg_len;    /* ヘッダーを含むメッセージの長さ */
    __u16 nlmsg_type;   /* メッセージの内容のタイプ */
    __u16 nlmsg_flags;  /* 追加フラグ */
    __u32 nlmsg_seq;    /* シーケンス番号 */
    __u32 nlmsg_pid;    /* 送信者のポート ID */
};

https://manpages.ubuntu.com/manpages/jammy/ja/man7/netlink.7.html

メッセージは取得したい情報によって異なりますが、ネットワークデバイスの情報が欲しい場合はifinfomsgを使用します

struct ifinfomsg {
    unsigned char  ifi_family; /* AF_UNSPEC */
    unsigned short ifi_type;   /* Device type */
    int            ifi_index;  /* Interface index */
    unsigned int   ifi_flags;  /* Device flags  */
    unsigned int   ifi_change; /* change mask */
};

https://manpages.ubuntu.com/manpages/jammy/ja/man7/rtnetlink.7.html

これらを使って以下のようにリクエストを送信することができます

#include <linux/rtnetlink.h>
#include <sys/socket.h>

struct request {
  struct nlmsghdr header;
  struct ifinfomsg ifinfo;
};

int main() {
  int rtnetlink_socket = socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_ROUTE);

  // リクエスト送信
  struct request req = {
    .header.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
    .header.nlmsg_type = RTM_GETLINK,
    .header.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,
    .header.nlmsg_seq = 0,
    .header.nlmsg_pid = 0,
    .ifinfo.ifi_family = AF_PACKET,
  };

  send(rtnetlink_socket, &req, sizeof(req), 0);
}

細かいパラメーターはipコマンドから流用しています
https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/lib/libnetlink.c?h=v6.1.0#n517

受信するメッセージは送信したリクエスト + データの形式で返ってきます
このとき、返ってくるメッセージの個数は任意です
今回のリクエストでは以下のデータが任意個返ってきます

struct rtattr {
    unsigned short rta_len;    /* Length of option */
    unsigned short rta_type;   /* Type of option */
    /* Data follows */
};

/* Data follows */と書いてありますがrtattr構造体はLTV形式でいうところのLengthとTypeだけの構造体で実際のデータはこの直後に格納されています
もう少し細かい作法はありますが、おおむね以下の形でデータを受信できます

  // データ受信
  char buf[32768];
  struct iovec iov = {
    .iov_base = &buf,
    .iov_len = 32768
  };
  struct sockaddr_nl nladdr = {
    .nl_family = AF_NETLINK
  };
  struct msghdr msg = {
    .msg_name = &nladdr,
    .msg_namelen = sizeof(nladdr),
    .msg_iov = &iov,
    .msg_iovlen = 1,
  };

  int n = recvmsg(rtnetlink_socket, &msg, 0);

上記によってbufの中にリクエストとデータが返ってきます
受信したデータ全体のバイト数はnです
出力したい場合はNetlink APIで用意されているマクロを使って以下のように書けます

  // 出力
  for (struct nlmsghdr *answer = (struct nlmsghdr *)msg.msg_iov->iov_base; NLMSG_OK(answer, n); answer = NLMSG_NEXT(answer, n)) {
    int attrlen = answer->nlmsg_len - sizeof(struct request);
    for (struct rtattr *attr = (struct rtattr *)((void *)answer + sizeof(struct request)); RTA_OK(attr, attrlen); attr = RTA_NEXT(attr, attrlen)) {
      if (attr->rta_type == IFLA_IFNAME) {
        printf("%s\n", (char *)RTA_DATA(attr));
      }
    }
  }
$ gcc main.c
$ ./a.out
lo
enp86s0

つまり、私たちは普段Netlink APIが返すrtattrの値を見ていることがわかりました
この値はLinuxカーネルの管理するnet_deviceという構造体からデータを取得しています
次はLinuxカーネルの中身を見ていきましょう

インターフェース名取得プログラム

#include <stdio.h>
#include <stdlib.h>
#include <linux/rtnetlink.h>
#include <sys/socket.h>

struct request {
  struct nlmsghdr header;
  struct ifinfomsg ifinfo;
};

int main() {
  int rtnetlink_socket = socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_ROUTE);

  // リクエスト送信
  struct request req = {
    .header.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
    .header.nlmsg_type = RTM_GETLINK,
    .header.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,
    .header.nlmsg_seq = 0,
    .header.nlmsg_pid = 0,
    .ifinfo.ifi_family = AF_PACKET,
  };

  send(rtnetlink_socket, &req, sizeof(req), 0);

  // データ受信
  char buf[32768];
  struct iovec iov = {
    .iov_base = &buf,
    .iov_len = 32768
  };
  struct sockaddr_nl nladdr = {
    .nl_family = AF_NETLINK
  };
  struct msghdr msg = {
    .msg_name = &nladdr,
    .msg_namelen = sizeof(nladdr),
    .msg_iov = &iov,
    .msg_iovlen = 1,
  };

  int n = recvmsg(rtnetlink_socket, &msg, 0);

  // 出力
  for (struct nlmsghdr *answer = (struct nlmsghdr *)msg.msg_iov->iov_base; NLMSG_OK(answer, n); answer = NLMSG_NEXT(answer, n)) {
    int attrlen = answer->nlmsg_len - sizeof(struct request);
    for (struct rtattr *attr = (struct rtattr *)((void *)answer + sizeof(struct request)); RTA_OK(attr, attrlen); attr = RTA_NEXT(attr, attrlen)) {
      if (attr->rta_type == IFLA_IFNAME) {
        printf("%s\n", (char *)RTA_DATA(attr));
      }
    }
  }
}

Linuxカーネルは"eth%d"形式でnet_deviceの名前を初期化する

今更ですがenp1s0という名前はPCIバスに接続されたEthernetデバイスに付けられます
ループバックデバイスloや無線デバイスは異なる命名規則で名前が付けられますが、今回はこれらのデバイスは説明の対象外とします
以降はPCIバスに接続されたEthernetデバイスに限定して話を進めます

net_deviceはLinuxカーネルがネットワーク機器を管理するための構造体です
仮想デバイスも物理デバイスも機能に関するパラメーターや名前空間も全て管理する構造体なので非常に大きくて複雑な構造体になっています
このnet_deviceの初期化はデバイス毎に異なっており各デバイスのデバイスドライバで初期化が実行されます
例えば、今回の環境で接続されているenp86s0はlspciの出力からIntelのigcデバイスドライバで初期化されていることがわかります

$ lspci -nn -k
56:00.0 Ethernet controller [0200]: Intel Corporation Ethernet Controller I225-V [8086:15f3] (rev 03)
        Subsystem: Intel Corporation Ethernet Controller I225-V [8086:3024]
        Kernel driver in use: igc
        Kernel modules: igc

Linuxカーネルは主要なデバイスドライバをカーネル内部に最初から組み込んでいます
igcデバイスドライバであればdrivers/net/ethernet/intel/igc/にデバイスドライバが存在します
みなさんの環境で使用しているデバイスドライバも大体はLinuxカーネルの中を探せばどこかに存在しているはずです
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/net/ethernet/intel/igc/igc_main.c

このigcデバイスドライバはpci_driver構造体を持っており、このpci_driver構造体がLinuxカーネルで管理されています

static struct pci_driver igc_driver = {
	.name     = igc_driver_name,
	.id_table = igc_pci_tbl,
	.probe    = igc_probe,
	.remove   = igc_remove,
	.driver.pm = pm_ptr(&igc_pm_ops),
	.shutdown = igc_shutdown,
	.err_handler = &igc_err_handler,
};

id_tableはこのデバイスドライバで操作できるデバイスを列挙するデータです
igcの場合は以下が定義されています

static const struct pci_device_id igc_pci_tbl[] = {
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I225_LM), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I225_V), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I225_I), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I220_V), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I225_K), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I225_K2), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I226_K), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I225_LMVP), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I226_LMVP), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I225_IT), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I226_LM), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I226_V), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I226_IT), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I221_V), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I226_BLANK_NVM), board_base },
	{ PCI_VDEVICE(INTEL, IGC_DEV_ID_I225_BLANK_NVM), board_base },
	/* required last entry */
	{0, }
};

上記はVendor IDがINTEL(8086)、Device IDがIGC_DEV_ID_I225_LM(15f2)やIGC_DEV_ID_I225_V(15f3)のデバイスはigcデバイスドライバで操作できることを示しています

net_deviceの命名について重要なのはpci_driver構造体の関数ポインタprobeです
Linuxカーネルは起動時にPCIバスに接続されている全てのPCIデバイスの走査してVendor IDとDevice IDを取得します
このとき取得したVendor IDおよびDevice IDに対応するpci_driverを発見するとprobe()を呼び出してPCIデバイスの初期化を実行します
igcに限らず大体のEthernetデバイスのデバイスドライバはこのprobe()の中でnet_deviceの初期化を実行しています

実際にigcのprobeの中身を見ていきましょう
igcのprobeはigc_probeに定義されています

static int igc_probe(struct pci_dev *pdev,
		     const struct pci_device_id *ent)
{
	struct igc_adapter *adapter;
	struct net_device *netdev;
	struct igc_hw *hw;
	const struct igc_info *ei = igc_info_tbl[ent->driver_data];
	int err;
...
	netdev = alloc_etherdev_mq(sizeof(struct igc_adapter),
				   IGC_MAX_TX_QUEUES);
...

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/net/ethernet/intel/igc/igc_main.c#n7104
igc_probeの中ではMACアドレスや使用可能な機能などを設定しています
net_deviceの生成を行っているのはalloc_etherdev_mqで、この中でデバイスの名前を初期化しています
alloc_etherdev_mqはマクロで実態はalloc_etherdev_mqsになりますが、このalloc_netdev_mqsの中身を見るとnet_deviceが"eth%d"形式で初期化されていることが確認できます

struct net_device *alloc_etherdev_mqs(int sizeof_priv, unsigned int txqs,
				      unsigned int rxqs)
{
	return alloc_netdev_mqs(sizeof_priv, "eth%d", NET_NAME_ENUM,
				ether_setup, txqs, rxqs);
}


udevはsystemd傘下のプロジェクトです
ネットワークデバイスに限らず/dev以下のデバイスはudevが管理しています
udevはユーザープログラムのためudev開始時点でnet_deviceはLinuxカーネルによって"eth%d"形式で名前が初期化されています
これらのデバイスはsysfsによって/sys/以下に配備され参照可能な状態になっています
udevは内部に/sys/以下のデバイスのラッパーであるsd_device構造体を持っています
(正確にはsystemdの管理する構造体です)
udevは/sys/を参照してsd_deviceを初期化したら各デバイスに対応したワーカープロセスを起動しデバイスを管理します
このときudevは設定ファイルに従って各デバイスの初期化を実行します
インターフェース名の変更はそのものズバリのrename_netifで実装されており、NETLINK_ROUTEによってインターフェース名を変更しています

Discussion