Linux Kernel: rtnl_mutex を長時間 ロックして刺さった状態を観察する

Linux Kernel で struct net_device や Routing Netlink の処理を排他制御する mutex である  rtnl_mutex を任意の時間 ロックするカーネルモジュールを作成して、rtnl_mutex のロックがユーザランドのプロセスやカーネルスレッド等にどういった影響を及ぼすかを観察する

... というエントリです

Linux カーネルの話や、strace, gdb, /proc/$pid/stack を使ったデバッグ方法に興味あるかた向けです

経緯

とあるサーバで動いてる大量のプロセスが rtnl_mutex をロックする箇所やそれに関連していそうな箇所で TASK_UNINTERRUPTIBLE でブロックしてしまい、障害を起こしているのに遭遇して原因を追いかけていたのでした

障害の再現が難しくて困っていたのだが、別のアプローチとして 「rtnl_mutex を自分で書いたカーネルモジュールで強制的にロックしてしまえば、どんなプロセスが刺さったりするかの観察くらいはできるんじゃね?」 と思いつく

肝心の障害の原因はわからんままだけど、擬似で障害時の状態を再現して何かしらヒントや解決の糸口を見出せそうと考えた

  • どんなプロセスが、
  • どんなライブラリ関数を呼び出して、
  • どんなシステムコールでブロックするのか?

を調べていけそう

ところで rtnl_mutex とは?

Ubuntu Xenial 4.4系のカーネルをベースに話を進めます. rtnl_mutex の定義は下記の通りです

/* net/core/rtnetlink.c */
static DEFINE_MUTEX(rtnl_mutex); 👈

static DEFINE_MUTEX(rtnl_mutex); として定義されているので、この mutex を扱う箇所はカーネル内で必ずシリアルに実行される = 並列/並行 で動かない、と理解できる。

rtnl_mutex を触るための API は rtnl_lock(), rtnl_unlock() として定義されている

void rtnl_lock(void)
{
    mutex_lock(&rtnl_mutex);
}
EXPORT_SYMBOL(rtnl_lock); 

void rtnl_unlock(void)
{
    /* This fellow will unlock it for us. */
    netdev_run_todo();
}
EXPORT_SYMBOL(rtnl_unlock); 

rtnl_mutex は何を保護する mutex なのか?

この mutex がどんなリソースを保護しているかは下記が参考になりました

lists.kernelnewbies.org

This lock is used to serialize changes to net_device instances from runtime events, conf changes

stackoverflow.com

net_device content changes are taken care of by the Routing Netlink semaphore (rtnl_sem), which is acquired and released with rtnl_lock and rtnl_unlock, respectively.[*] This semaphore is used to serialize changes to net_device instances from:

  • Runtime events

For example, when the link state changes (e.g., a network cable is plugged or unplugged), the kernel needs to change the device state by modifying dev->flags.

  • Configuration changes When the user applies a configuration change with commands such as ifconfig and route from the net-tools package, or ip from the IPROUTE2 package, the kernel is > notified via ioctl commands and the Netlink socket, respectively. The routines invoked via these interfaces must use locks.

鈍器としかえいないこの本にも解説がちらっとかいてある

Understanding Linux Network Internals: Guided Tour to Networking on Linux

Understanding Linux Network Internals: Guided Tour to Networking on Linux

Changes to net_device structures are protected with the Routing Netlink semaphore via rtnl_lock and rtnl_lock, which is way register_netdev acquires the lock(semaphore) at the beginning and releases it before returning.

P.150

  • struct net_device を保護する
  • RT = Routing Netlink の略称らしい

rtnl_lock() を呼び出している箇所はあちこちにあって、ドライバ等の実装も含めると 471箇所 もあった (ソースは 4.4系)

🔨 rtnl_mutex を掴むカーネルモジュールをつくる

rtnl_lock() と rtnl_unlock() は EXPORT_SYMBOL されているので、自作カーネルモジュールからも利用できて実験に都合がよい

void rtnl_lock(void)
{
    mutex_lock(&rtnl_mutex);
}
EXPORT_SYMBOL(rtnl_lock); 👈

void rtnl_unlock(void)
{
    /* This fellow will unlock it for us. */
    netdev_run_todo();
}
EXPORT_SYMBOL(rtnl_unlock); 👈

この API を使い 「 rtnl_mutex でロックをとって 一定時間何もせずに待つ」 というカーネルモジュールをこしらえた

あたかも rtnl_lock 内のクリティカルセクションが長くなり刺さった状態をひきおこし、その他のプロセスやカーネルスレッドにどんな影響がでるかを観察することができる

github.com

sysctl インタフェース経由で rtnl_lock() を呼び出して rtnl_mutex を任意の時間だけ掴むことができる

# 10秒間 rtnl_lock() で mutex を掴む
sysctl -w rtnl_stuck=10

sysctl インタフェースは vm.drop_caches の実装を真似して コピペして 作りました ^^

実験の手順

  1. rtnl_lock() で rtnl_mutex を適当な時間 つかんでおく
sysctl -w rtnl_stuck=60
  1. rtnl_lock() でブロックしそうなコマンドを探す
  2. ブロックしたプロセスを観察する
  3. 該当のプロセスの /proc/$pid/stack を調べてカーネルソースを読んだり
  4. strace をとったり
  5. gdb でバックトレースをとったりして、あれこれ観察する

という感じで進めました

🔎 sudo

まずは sudo が刺さるのを調べた. ps で wchan を取ると netlink_dump_start がでる

$ ps ax -ostate,pid,cmd,wchan | grep ^D                  

D 28867 sudo                        netlink_dump_start 👈

/proc/$pid/stack をみてカーネルモードのバックトレースを採取すると以下の通りになる

root@xenial:~# cat /proc/3459/stack
[<0>] __netlink_dump_start+0x51/0x1d0
[<0>] rtnetlink_rcv_msg+0x1a2/0x290
[<0>] netlink_rcv_skb+0xd9/0x110
[<0>] rtnetlink_rcv+0x15/0x20
[<0>] netlink_unicast+0x198/0x260
[<0>] netlink_sendmsg+0x2e2/0x3d0
[<0>] sock_sendmsg+0x3e/0x50
[<0>] SYSC_sendto+0x101/0x190
[<0>] SyS_sendto+0xe/0x10
[<0>] do_syscall_64+0x73/0x130
[<0>] entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[<0>] 0xffffffffffffffff

ソースを読む

__netlink_dump_start のどこでブロックしているかをソースで確かめる

int __netlink_dump_start(struct sock *ssk, struct sk_buff *skb,
             const struct nlmsghdr *nlh,
             struct netlink_dump_control *control)
{
    struct netlink_callback *cb;
    struct sock *sk;
    struct netlink_sock *nlk;
    int ret;

    nlk = nlk_sk(sk);
    mutex_lock(nlk->cb_mutex); 👈 
    /* A dump is in progress... */
    if (nlk->cb_running) {
        ret = -EBUSY;
        goto error_unlock;
    }
    /* add reference of module which cb->dump belongs to */
    if (!try_module_get(control->module)) {
        ret = -EPROTONOSUPPORT;
        goto error_unlock;
    }

... 略

mutex_lock(nlk->cb_mutex); らしい. では次に nlk->cb_mutex がどこで初期化されているかを探して下記にたどり着く。

rtnl_mutex 発見だ!!! 🔎

static int __net_init rtnetlink_net_init(struct net *net)
{
    struct sock *sk;
    struct netlink_kernel_cfg cfg = {
        .groups     = RTNLGRP_MAX,
        .input      = rtnetlink_rcv,
        .cb_mutex   = &rtnl_mutex, 👈
        .flags      = NL_CFG_F_NONROOT_RECV,
    };

    sk = netlink_kernel_create(net, NETLINK_ROUTE, &cfg);
    if (!sk)
        return -ENOMEM;
    net->rtnl = sk;
    return 0;
}

ブロックしたシステムコールを strace で調べる

strace で該当のシステムコールを調べると AF_NETLINK + NETLINK_ROUTE のソケットに対して、 sendto(2) で RTM_GETLINK を指定している

socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE) = 3
bind(3, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12) = 0
getsockname(3, {sa_family=AF_NETLINK, nl_pid=3828, nl_groups=00000000}, [12]) = 0
sendto(3, 👈 {{len=20, type=RTM_GETLINK, flags=NLM_F_REQUEST|NLM_F_DUMP, seq=1572101252, pid=0}, {ifi_family=AF_UNSPEC, ...}}, 20, 0, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12

gdb でライブラリの呼び出しを調べる

gdb で該当のシステムコールを呼び出した際のユーザモードでのバックトレースを取ると、getifaddrs(3) を呼び出したことがわかる

(gdb) bt
#0  sendto () at ../sysdeps/unix/syscall-template.S:84
#1  0x00007fa677594f53 in __netlink_sendreq (h=0x7ffc8a486060, h=0x7ffc8a486060, type=18) at ../sysdeps/unix/sysv/linux/ifaddrs.c:119
#2  __netlink_request (h=h@entry=0x7ffc8a486060, type=type@entry=18) at ../sysdeps/unix/sysv/linux/ifaddrs.c:157
#3  0x00007fa6775952ac in getifaddrs_internal (ifap=ifap@entry=0x7ffc8a4860e8) at ../sysdeps/unix/sysv/linux/ifaddrs.c:334
#4  0x00007fa677596000 in __getifaddrs 👈(ifap=ifap@entry=0x7ffc8a4860e8) at ../sysdeps/unix/sysv/linux/ifaddrs.c:828
#5  0x000055bfabe231bb in get_net_ifs (addrinfo=addrinfo@entry=0x7ffc8a486208) at /build/sudo-jJ9KEJ/sudo-1.8.16/src/net_ifs.c:125
#6  0x000055bfabe24c64 in parse_args (argc=1, argv=0x7ffc8a486518, nargc=0x7ffc8a48629c, nargv=0x7ffc8a4862a0, settingsp=0x7ffc8a4862c8, env_addp=0x7ffc8a4862a8)
    at /build/sudo-jJ9KEJ/sudo-1.8.16/src/parse_args.c:195
#7  0x000055bfabe19bd7 in main (argc=1, argv=0x7ffc8a486518, envp=0x7ffc8a486528) at /build/sudo-jJ9KEJ/sudo-1.8.16/src/sudo.c:207
(gdb) 

ここまで調べて sudo が getifaddrs(3) を呼び出しているのがわかりました

linuxjm.osdn.jp

ip -ass -a も同様にブロックします

🔎ifconfig

ifconfig も刺さります

$ ps ax -ostate,pid,cmd,wchan | grep ^D 
D 10668 ifconfig        rtnl_lock

ブロックしたシステムコールを strace で調べる

ioctrl(2) + SIOCGIFCONF らしいです

ioctl(4, SIOCGIFCONF, {30 * sizeof(struct ifreq)

ここらへんのシステムコールは自分で触ることが少なくて馴染みがないですね ...

SIOCGIFCONF インターフェースの (トランスポート層の) アドレスのリストを返す。 現在のところ、互換性のため返されるのは AF_INET (IPv4) 系のアドレスだけである。 > 他の操作と違い、この ioctl では ifconf 構造体を渡す。

https://linuxjm.osdn.jp/html/LDP_man-pages/man7/netdevice.7.html

カーネルモードのバックトレース を調べる

dev_ioctl の中でブロックしていそうです

root@xenial:~# cat /proc/11480/stack
[<ffffffff81746455>] rtnl_lock+0x15/0x20
[<ffffffff817506df>] dev_ioctl+0x32f/0x580
[<ffffffff817174b2>] sock_do_ioctl+0x42/0x50
[<ffffffff817179b2>] sock_ioctl+0x1d2/0x290
[<ffffffff812231cf>] do_vfs_ioctl+0x29f/0x490
[<ffffffff81223439>] SyS_ioctl+0x79/0x90
[<ffffffff81841eb2>] entry_SYSCALL_64_fastpath+0x16/0x71
[<ffffffffffffffff>] 0xffffffffffffffff

ソースを調べる

dev_ioctl のソースを読みます。

/**
 * dev_ioctl   -   network device ioctl
 * @net: the applicable net namespace
 * @cmd: command to issue
 * @arg: pointer to a struct ifreq in user space
 *
 * Issue ioctl functions to devices. This is normally called by the
 * user space syscall interfaces but can sometimes be useful for
 * other purposes. The return value is the return from the syscall if
 * positive or a negative errno code on error.
 */

int dev_ioctl(struct net *net, unsigned int cmd, void __user *arg)
{
    struct ifreq ifr;
    int ret;
    char *colon;

    /* One special case: SIOCGIFCONF takes ifconf argument
      and requires shared lock, because it sleeps writing
      to user space.
    */

    if (cmd == SIOCGIFCONF) {
        rtnl_lock(); 👈
        ret = dev_ifconf(net, (char __user *) arg);
        rtnl_unlock();
        return ret; 👈
    }

network device をみてインタフェースの情報を取り出すので、rtnl_lock() で保護しているのですね

🔎 ip netns add, unshare

ip netns add <名前空間の名前>unshare を呼び出すと unshare(2) でブロックする

カーネルモードのバックトレース を調べる

root@xenial:~# cat /proc/4399/stack
[<0>] rtnl_lock+0x15/0x20
[<0>] register_netdev+0x12/0x30
[<0>] loopback_net_init+0x4d/0xa0
[<0>] ops_init+0x44/0x130
[<0>] setup_net+0xb6/0x170
[<0>] copy_net_ns+0xbf/0x230
[<0>] create_new_namespaces+0x11b/0x1e0
[<0>] unshare_nsproxy_namespaces+0x5a/0xb0
[<0>] SyS_unshare+0x1f3/0x390
[<0>] do_syscall_64+0x73/0x130
[<0>] entry_SYSCALL_64_after_hwframe+0x3d/0xa2
[<0>] 0xffffffffffffffff

ブロックしたシステムコールを strace で調べる

自明っぽいですが、unshare(2) でブロックしてます

mmap(NULL, 2981280, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f54796a7000
close(3)                                = 0
unshare(CLONE_NEWNET 👈

ソースを読む

register_netdevice を挟むように rtnl_mutex をとっていてわかりやすい. struct net_device に変更を入れるからだろう

/**
 * register_netdev - register a network device
 * @dev: device to register
 *
 * Take a completed network device structure and add it to the kernel
 * interfaces. A %NETDEV_REGISTER message is sent to the netdev notifier
 * chain. 0 is returned on success. A negative errno code is returned
 * on a failure to set up the device, or if the name is a duplicate.
 *
 * This is a wrapper around register_netdevice that takes the rtnl semaphore
 * and expands the device name if you passed a format string to
 * alloc_netdev.
 */
int register_netdev(struct net_device *dev)
{
    int err;

    rtnl_lock(); 👈
    err = register_netdevice(dev);
    rtnl_unlock(); 👈
    return err; 
}
EXPORT_SYMBOL(register_netdev);

🔎 unshare を二度呼び出すと???

ブロックしている unshare(2) がいる状態でさらにもう一度 unshare(2) を呼び出すと、別の箇所 = copy_net_ns でブロックする

D 13703 unshare -n bash -c exit     rtnl_lock
D 13712 unshare -n bash -c exit     copy_net_ns 👈

rtnl_lock() を呼び出す前に、別のロックである mutex_lock(&net_mutex); でブロックしている

struct net *copy_net_ns(unsigned long flags,
            struct user_namespace *user_ns, struct net *old_net)
{
    struct net *net;
    int rv;

    if (!(flags & CLONE_NEWNET))
        return get_net(old_net);

    net = net_alloc();
    if (!net)
        return ERR_PTR(-ENOMEM);

    get_user_ns(user_ns);

    mutex_lock(&net_mutex); 🔑🔥
    rv = setup_net(net, user_ns);
    if (rv == 0) {
        rtnl_lock(); 🔑 🔥
        list_add_tail_rcu(&net->list, &net_namespace_list);
        rtnl_unlock();
    }
    mutex_unlock(&net_mutex);
    if (rv < 0) {
        put_user_ns(user_ns);
        net_drop_ns(net);
        return ERR_PTR(rv);
    }
    return net;
}

🔎 ip netns del ***

netns の削除では、作成時とは違って kworker カーネルスレッドがブロックする

D  5508 [kworker/u8:2]              rtnl_lock

詳細は省くが、 netns の削除はカーネルスレッドで非同期に扱われている

ブロックしたシステムコールを strace で調べる

カーネルスレッドなのでシステムコール呼び出しは無い

カーネルスレッドモードでのバックトレースを調べる

root@xenial:~# cat /proc/5648/stack
[<0>] rtnl_lock+0x15/0x20
[<0>] cleanup_net+0x9a/0x2b0
[<0>] process_one_work+0x14d/0x410
[<0>] worker_thread+0x4b/0x460
[<0>] kthread+0x105/0x140
[<0>] ret_from_fork+0x35/0x40
[<0>] 0xffffffffffffffff

ソースを読む

ソースを調べると cleanup_net でブロックしているのだと分かる

static void cleanup_net(struct work_struct *work)
{
    const struct pernet_operations *ops;
    struct net *net, *tmp;
    struct list_head net_kill_list;
    LIST_HEAD(net_exit_list);

    /* Atomically snapshot the list of namespaces to cleanup */
    spin_lock_irq(&cleanup_list_lock);
    list_replace_init(&cleanup_list, &net_kill_list);
    spin_unlock_irq(&cleanup_list_lock);

    mutex_lock(&net_mutex);

    /* Don't let anyone else find us. */
    rtnl_lock(); 👈
    list_for_each_entry(net, &net_kill_list, cleanup_list) {
        list_del_rcu(&net->list);
        list_add_tail(&net->exit_list, &net_exit_list);
        for_each_net(tmp) {
            int id;

            spin_lock_irq(&tmp->nsid_lock);
            id = __peernet2id(tmp, net);
            if (id >= 0)
                idr_remove(&tmp->netns_ids, id);
            spin_unlock_irq(&tmp->nsid_lock);
            if (id >= 0)
                rtnl_net_notifyid(tmp, RTM_DELNSID, id);
        }
        spin_lock_irq(&net->nsid_lock);
        idr_destroy(&net->netns_ids);
        spin_unlock_irq(&net->nsid_lock);

    }
    rtnl_unlock(); 👈

    /*
    * Another CPU might be rcu-iterating the list, wait for it.
    * This needs to be before calling the exit() notifiers, so
    * the rcu_barrier() below isn't sufficient alone.
    */
    synchronize_rcu();
    
    /* Run all of the network namespace exit methods */
    list_for_each_entry_reverse(ops, &pernet_list, list)
        ops_exit_list(ops, &net_exit_list);

    /* Free the net generic variables */
    list_for_each_entry_reverse(ops, &pernet_list, list)
        ops_free_list(ops, &net_exit_list);

    mutex_unlock(&net_mutex);

    /* Ensure there are no outstanding rcu callbacks using this
    * network namespace.
    */
    rcu_barrier();

    /* Finally it is safe to free my network namespace structure */
    list_for_each_entry_safe(net, tmp, &net_exit_list, exit_list) {
        list_del_init(&net->exit_list);
        put_user_ns(net->user_ns);
        net_drop_ns(net);
    }
}
static DECLARE_WORK(net_cleanup_work, cleanup_net);

netns の作成削除に関しての余談

netns の作成/削除の際に rtnl_lock() を掴んでボトルネックになることに対して、すでに過去に LKML にパッチが投稿されており ロックの粒度/スコープが小さくなるように改善がなされている

lore.kernel.org

たしか 4.17 以降で取り込まれています

🔎 その他のプロセス/コマンド

nginx や apache2 なども起動時に名前解決をしようとする際に netlink + NETLINK_ROUTE でブロックするの確認している。長くなるので詳細は省きます

まとめ

  • netlink や インタフェースの情報をとるあたりで ガシガシ rtnl_lock とってるのだと理解した
  • カーネルソースコードを追っていてもロック周り読み飛ばしがちだが、並列処理やボトルネックを生む箇所を把握できるよう おさえておきたい
  • カーネルのバージョン上げると ブロックしなくなってるものもありました。4.4 系の話としておさえてください
  • bpftrace つかって調べるの忘れていたので、追試で調べます