それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

最速のEchoサーバーを目指して、LinuxKernelモジュールを作っていく part4

はじめに

前回、実装したはずの機能で特にacceptでコケる問題がなかなか解決出来なかったので

毎回おなじみになりつつあるこのサイト
linux/include - Elixir - Free Electrons
さらに今回は、この本も参考にして書いていく
www.oreilly.co.jp

まず色々、調べたりしたことをまとめてその後に本題に入ろうと思う

色々やる前のメモ

実際に動いたであろうサンプルソースを参考にああだこうだ考えながら書いていたがacceptでコケる。
省略していた待ち行列が関係してるのではと思っている。

そもそも、エラーコード返ってきてるのにそれを見てなかったから見てみる。
見てみた。

$dmesg
....
....
[524524.095520] Fastest Echo Server Start!!
[524524.095544] accept error no:-11
$

なんか、acceptでエラーが返ってきている。
それを上記のサイトで調べる。
まずはacceptの実装から。
acceptの実装は次のようになっていた。

static int accept(struct socket *sock, struct socket *new_sock, int flags)
{
	struct sock *sk = sock->sk;
	struct sk_buff *buf;
	int res;

	lock_sock(sk);

	if (sock->state != SS_LISTENING) {
		res = -EINVAL;
		goto exit;
	}

	while (skb_queue_empty(&sk->sk_receive_queue)) {
		if (flags & O_NONBLOCK) {
			res = -EWOULDBLOCK;
			goto exit;
		}
		release_sock(sk);
		res = wait_event_interruptible(*sk_sleep(sk),
				(!skb_queue_empty(&sk->sk_receive_queue)));
		lock_sock(sk);
		if (res)
			goto exit;
	}

	buf = skb_peek(&sk->sk_receive_queue);

	res = tipc_create(sock_net(sock->sk), new_sock, 0, 0);
	if (!res) {
		struct sock *new_sk = new_sock->sk;
		struct tipc_sock *new_tsock = tipc_sk(new_sk);
		struct tipc_port *new_tport = new_tsock->p;
		u32 new_ref = new_tport->ref;
		struct tipc_msg *msg = buf_msg(buf);

		lock_sock(new_sk);

		/*
		 * Reject any stray messages received by new socket
		 * before the socket lock was taken (very, very unlikely)
		 */

		reject_rx_queue(new_sk);

		/* Connect new socket to it's peer */

		new_tsock->peer_name.ref = msg_origport(msg);
		new_tsock->peer_name.node = msg_orignode(msg);
		tipc_connect2port(new_ref, &new_tsock->peer_name);
		new_sock->state = SS_CONNECTED;

		tipc_set_portimportance(new_ref, msg_importance(msg));
		if (msg_named(msg)) {
			new_tport->conn_type = msg_nametype(msg);
			new_tport->conn_instance = msg_nameinst(msg);
		}

		/*
		 * Respond to 'SYN-' by discarding it & returning 'ACK'-.
		 * Respond to 'SYN+' by queuing it on new socket.
		 */

		if (!msg_data_sz(msg)) {
			struct msghdr m = {NULL,};

			advance_rx_queue(sk);
			send_packet(NULL, new_sock, &m, 0);
		} else {
			__skb_dequeue(&sk->sk_receive_queue);
			__skb_queue_head(&new_sk->sk_receive_queue, buf);
		}
		release_sock(new_sk);
	}
exit:
	release_sock(sk);
	return res;
}

この中で、一通り関係しそうなものをerror.hとかerror-base.hから探してみる。
そうすると

#define EWOULDBLOCK EAGAIN

が返っていることがわかり今度はこのEAGAINを調べた。
すると、こいつが例の11番のエラーを返していた。
ただ

#define EAGAIN 11 //try again

とあり、何をtry againすればいいのかわからなかった。
そこでaccept関数の実装をみて、どこでこのエラーが返ってきてるか調べてみることにした。

するとここの処理が影響していることがわかった。

while (skb_queue_empty(&sk->sk_receive_queue)) {
		if (flags & O_NONBLOCK) {
			res = -EWOULDBLOCK;
			goto exit;
		}
		release_sock(sk);
		res = wait_event_interruptible(*sk_sleep(sk),
				(!skb_queue_empty(&sk->sk_receive_queue)));
		lock_sock(sk);
		if (res)
			goto exit;
	}

skb_queue_emptyが返す値が真であるならO_NONBLOCKをflagsとして渡しているので
今回のgotoで該当の処理がスキップされエラーとなってしまうためskb_queue_emptyが返す値が偽である必要がある。

skb_queue_empty関数に渡しているものは

&sk->sk_receive_queue

という構造体の値である。
そもそもこのskが何かというとaccept関数の先頭で宣言、初期化されている次のものである。

struct sock *sk = sock->sk;

右辺のsockはaccept関数に渡している第一引数である。
そして問題のskb_queue_empty関数は次のようになっている。

static inline int skb_queue_empty(const struct sk_buff_head *list)
{
	return list->next == (struct sk_buff *)list;
}

これはソケットバッファに関するキューの様でリストの次の要素と先頭要素が等価であることを調べている(っぽい)。

これとはまた別にO_NONBLOCK、ノンブロッキングIOについてもよくわかっていなかったのでそれも別途記述していく。

ノンブロッキングI/O

カーネルモジュールで、TCPソケットを使おうとしたらほとんどのソースにO_NONBLOCKが出てきてなんだこれはと調べると
次のサイトに色々まとまっていた。

blog.takanabe.tokyo

ノンブロッキングI/OではI/O対象のファイルディスクリプタの準備完が了していないことをアプリケーション側に伝えるため即座にエラーが返る(errnoにEGAINが格納されて返ってくる)。一般に、O_NONBLOCKフラグを利用してノンブロッキングモードを宣言するが、この時プロセスはブロック状態にならず、CPUを他の処理に回すことができるためI/O待ち時間を有効活用できる。

ノンブロッキングI/Oはソケットなどのネットワークに用いられるI/OモデルでディスクI/Oには使わない。

これに関連していたのがO_NONBLOCKを使用していたacceptであった。
次のManPageを参照すると色々わかった。
Man page of ACCEPT

キューに保留となっている接続要求がなく、 かつソケットが非停止になっていないときは、 accept() は接続が発生するまで呼び出し元を停止 (block) する。 ソケットが非停止になっていて、 待ち状態の接続要求がキューに無いときは、 accept() はエラー EAGAIN か EWOULDBLOCK で失敗する。

これが最も自分を苦しめたあれである。
そもそも今回は、このキューを使っていなかった。
そのため、待ち状態の接続要求がキューになく(そもそもキューを用意していなく)、acceptがEAGAIN,EWOULDBLOCKを返していた・

おわりに

そんなこんなで、tcpクライアントからメッセージを受け取り表示するところまではできた。
コードは以下のレポジトリにある。
github.com
使ったtcpクライアントはpythonで書いたものを使用した。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket

target = "localhost"
port = 8888

#using IPv4 and TCP/IP
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#connecting
client_socket.connect( (target, port) )

#data send
client_socket.send("hello,world")

#get data
response_data = client_socket.recv(1024) 

print response_data

ただ、データの送信が出来ないことに加えて、時折メッセージを受信出来なかったりするのでそのあたりのバグとリファクタリングは今後も行っていく必要がある