情弱ログ

参考にならないので当てにしないでください

カーネルからリンクダウン時のイベントを飛ばす処理を探す

あるインタフェースのリンク状態を確認するために、netlinkソケットを使ってカーネルからメッセージを受信するサンプルがnetlink(7)に載っています。

struct sockaddr_nl sa;

memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
sa.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR;

fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
bind(fd, (struct sockaddr *) &sa, sizeof(sa));

このRTMGRP_LINKなパケットがどこから飛んでくるのかを調べたのでメモ。


まず、ag(the silver searcher)でLinuxカーネル(linux-4.4.26-gentoo)をgrepすると以下の結果が得られました。

[vicco@localhost] ~
% ag RTMGRP_LINK /usr/src/linux 
/usr/src/linux/include/uapi/linux/rtnetlink.h
572:#define RTMGRP_LINK		1

/usr/src/linux/Documentation/networking/operstates.txt
25:operation RTM_GETLINK. It is also possible to subscribe to RTMGRP_LINK
136:are multicasted on the netlink group RTMGRP_LINK.
140:-subscribe to RTMGRP_LINK

(*´・ω・`).o0(使えねぇ…)

とりあえず関係しそうなコードは${KERNEL}/net/core辺りにあること、リンクダウン時はドライバで何かしらの処理がありそうだということをカーネルに精通した先輩から聞いているので、その辺りから探してみます。
まず、${KERNEL}/drivers/net/ethernet/intel/e1000e/から"down"でgrepをかけて、引っかかったnetdev.cのe1000e_down()とかいう臭そうなところから見ていきます。EmacsならM-x grep-findすればエディタ内でgrepできて便利です(ステマ)。

/**
 * e1000e_down - quiesce the device and optionally reset the hardware
 * @adapter: board private structure
 * @reset: boolean flag to reset the hardware or not
 */
void e1000e_down(struct e1000_adapter *adapter, bool reset)
{
...
	/* signal that we're down so the interrupt handler does not
	 * reschedule our watchdog timer
	 */
	set_bit(__E1000_DOWN, &adapter->state);

	netif_carrier_off(netdev);

	/* disable receives in the hardware */
	rctl = er32(RCTL);
	if (!(adapter->flags2 & FLAG2_NO_DISABLE_RX))
		ew32(RCTL, rctl & ~E1000_RCTL_EN);
	/* flush and sleep below */

	netif_stop_queue(netdev);
...

netif_carrier_off()がそれっぽい。ググったらLinux Cross Referenceとかいうサイトから${KERNEL}/net/sched/sch_generic.cにあることが分かったので、見てみると以下の様な感じになっていました。(ところでカーネル開発者の人ってタグジャンプを使ってるんでしょうか。make TAGSでタグファイルを生成できるので少なからず使ってる人はいるんでしょうが、タグファイルを開くだけで1分以上待たされますし、メモリを滅茶苦茶食う上にもっさりとジャンプするので、諦めてググってからファイルを開いてます。ただのスペ貧では)

/**
 *	netif_carrier_off - clear carrier
 *	@dev: network device
 *
 * Device has detected loss of carrier.
 */
void netif_carrier_off(struct net_device *dev)
{
	if (!test_and_set_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
		if (dev->reg_state == NETREG_UNINITIALIZED)
			return;
		atomic_inc(&dev->carrier_changes);
		linkwatch_fire_event(dev);
	}
}
EXPORT_SYMBOL(netif_carrier_off);

linkwatch_fire_event()、臭いっすね。${KERNEL}/net/core/link_watch.cにあるらしいので見てみます。

void linkwatch_fire_event(struct net_device *dev)
{
	bool urgent = linkwatch_urgent_event(dev);

	if (!test_and_set_bit(__LINK_STATE_LINKWATCH_PENDING, &dev->state)) {
		linkwatch_add_event(dev);
	} else if (!urgent)
		return;

	linkwatch_schedule_work(urgent);
}
EXPORT_SYMBOL(linkwatch_fire_event);

linkwatch_add_eventを見てみます。同じファイル内にあるので、Emacsならsemanticを入れておけばC-c , jでその関数にジャンプできて便利です(ステマ)。

static void linkwatch_add_event(struct net_device *dev)
{
	unsigned long flags;

	spin_lock_irqsave(&lweventlist_lock, flags);
	if (list_empty(&dev->link_watch_list)) {
		list_add_tail(&dev->link_watch_list, &lweventlist);
		dev_hold(dev);
	}
	spin_unlock_irqrestore(&lweventlist_lock, flags);
}

lweventlistというリストに突っ込んでいることが分かりました。list_add_tailとかはググったら以下のサイトに詳しく書いてありました。カーネル初心者なので基本的なところが日本語で書いてあると安心します。
list_head構造体 - Linuxの備忘録とか・・・(目次へ)

とりあえずenqueueしてそうだな、ということが分かったので、どこかでキューを処理している関数があるはず。linkwatch_fire_event()に戻って、今度はlinkwatch_schedule_work()を見てみます。

static void linkwatch_schedule_work(int urgent)
{
...
	/* Minimise down-time: drop delay for up event. */
	if (urgent) {
		if (test_and_set_bit(LW_URGENT, &linkwatch_flags))
			return;
		delay = 0;
	}
...
	/*
	 * If urgent, schedule immediate execution; otherwise, don't
	 * override the existing timer.
	 */
	if (test_bit(LW_URGENT, &linkwatch_flags))
		mod_delayed_work(system_wq, &linkwatch_work, 0);
	else
		schedule_delayed_work(&linkwatch_work, delay);
...
}

引数のurgentがtrueならlinkwatch_flagsにアトミックにLW_URGENTビットを立てて、フラグを元に下のif文で分岐しているらしい。なんで下のif文でif(urgent)って書いたら駄目なの?とかはよく分かりません。多分同時に呼ばれると困るんだと勝手に思い込んでます。
遅延処理はググったら出てきたこのサイトが詳しかったです。ちなみにこれは学部の授業の資料っぽいです。あなおそろしや。
割り込みの後半部、Softirq、Tasklet、Work Queue
上の方でstatic DECLARE_DELAYED_WORK(linkwatch_work, linkwatch_event);という定義があったので、linkwatch_eventを探すとmutexロックして__linkwatch_run_queue()といういかにもキューを処理していそうな関数を呼び出しているので見てみます。

static void __linkwatch_run_queue(int urgent_only)
{
...
	LIST_HEAD(wrk);
...
	list_splice_init(&lweventlist, &wrk);

	while (!list_empty(&wrk)) {

		dev = list_first_entry(&wrk, struct net_device, link_watch_list);
		list_del_init(&dev->link_watch_list);

		if (urgent_only && !linkwatch_urgent_event(dev)) {
			list_add_tail(&dev->link_watch_list, &lweventlist);
			continue;
		}
		spin_unlock_irq(&lweventlist_lock);
		linkwatch_do_dev(dev);
		spin_lock_irq(&lweventlist_lock);
	}
...

wrkリストという新しいリストにlweventlistを引っ付けて、wrkリストに対し処理しているようです。
list_splice_init
lweventlistを直接処理しないのはurgent_onlyな処理のためなんですかね。とりあえず中を見てみるとlinkwatch_do_dev()というのが重要そうなので見てみます。

static void linkwatch_do_dev(struct net_device *dev)
{
	/*
	 * Make sure the above read is complete since it can be
	 * rewritten as soon as we clear the bit below.
	 */
	smp_mb__before_atomic();

	/* We are about to handle this device,
	 * so new events can be accepted
	 */
	clear_bit(__LINK_STATE_LINKWATCH_PENDING, &dev->state);

	rfc2863_policy(dev);
	if (dev->flags & IFF_UP) {
		if (netif_carrier_ok(dev))
			dev_activate(dev);
		else
			dev_deactivate(dev);

		netdev_state_change(dev);
	}
	dev_put(dev);
}

netdev_state_change()を見ていきます。${KERNEL}/net/core/dev.cにあります。

/**
 *	netdev_state_change - device changes state
 *	@dev: device to cause notification
 *
 *	Called to indicate a device has changed state. This function calls
 *	the notifier chains for netdev_chain and sends a NEWLINK message
 *	to the routing socket.
 */
void netdev_state_change(struct net_device *dev)
{
	if (dev->flags & IFF_UP) {
		struct netdev_notifier_change_info change_info;

		change_info.flags_changed = 0;
		call_netdevice_notifiers_info(NETDEV_CHANGE, dev,
					      &change_info.info);
		rtmsg_ifinfo(RTM_NEWLINK, dev, 0, GFP_KERNEL);
	}
}
EXPORT_SYMBOL(netdev_state_change);

IFF_UPはインタフェースが動作中であることを示すらしいです。この時点ではまだtrueなのかな。rtmsg_ifinfo()を追うために${KERNEL}/net/core/rtnetlink.cを見ていきます。

void rtmsg_ifinfo(int type, struct net_device *dev, unsigned int change,
		  gfp_t flags)
{
	struct sk_buff *skb;

	if (dev->reg_state != NETREG_REGISTERED)
		return;

	skb = rtmsg_ifinfo_build_skb(type, dev, change, flags);
	if (skb)
		rtmsg_ifinfo_send(skb, dev, flags);
}
EXPORT_SYMBOL(rtmsg_ifinfo);

同ファイルからrtmsg_ifinfo_send()を見てみます。

void rtmsg_ifinfo_send(struct sk_buff *skb, struct net_device *dev, gfp_t flags)
{
	struct net *net = dev_net(dev);

	rtnl_notify(skb, net, 0, RTNLGRP_LINK, NULL, flags);
}

RTNLGRP_LINKは${KERNEL}/include/uapi/linux/rtnetlink.hで以下のように定義されています。

/* RTnetlink multicast groups */
enum rtnetlink_groups {
	RTNLGRP_NONE,
#define RTNLGRP_NONE		RTNLGRP_NONE
	RTNLGRP_LINK,
#define RTNLGRP_LINK		RTNLGRP_LINK
	RTNLGRP_NOTIFY,
#define RTNLGRP_NOTIFY		RTNLGRP_NOTIFY
...
	__RTNLGRP_MAX	__RTNLGRP_MAX
};
#define RTNLGRP_MAX	(__RTNLGRP_MAX - 1)

あれ…RTMGRP_LINKは…?という疑問なんですが、同じくrtnetlink.hには#define RTMGRP_LINK 1という定義があり、RTNLGRP_LINKも1であることから、別名がついてるだけなのかな?というあやふやな感じです。別名がついているにしては、その下のRTNLGRP_NEIGHは3なのに対し、RTMGRP_NEIGHは4だったりするので正直分かってません。何なんだろう…

以上、煮え切らない感じで終わってしまいましたが、カーネルからnetlinkでRTM_NEWLINKをユーザランドに飛ばすまでの処理を見てきました。この後、ユーザランドでは以下のサイトのように、netlinkのメッセージのタイプがRTM_NEWLINKかつ、インタフェースのフラグのIFF_UPビットがtrueならup、falseならdownといった処理でインタフェースの状態を確認できます。(IFF_UPフラグ、どこでfalseにしたんだろう…)
notifications - How to get notified about network interface changes with Netlist and RTMGRP_LINK signal? - Stack Overflow