hana_shinのLinux技術ブログ

Linuxの技術情報を掲載しています。特にネットワークをメインに掲載していきます。

Netlinkプログラミングの書き方



1 Netlinkとは?

Netlinkとは、ユーザー空間とカーネル空間で情報をやりとりするためのインタフェースです。以下は、ユーザプロセスとカーネルモジュールがNetlinkインタフェースを使ってやり取りをしているところを図示したものです。

ユーザ空間
              +-----------------------+
              |                       |
              |     ユーザプロセス    |
              |                       |
              +-----------------------+
                          ↑
                          |
--------------------------|------------ Netlinkインタフェース
カーネル空間              |
                          ↓
              +-----------------------+
              |                       |
              |   カーネルモジュール  |
              |                       |
              +-----------------------+

Netlinkを使ってメッセージを送信するには、socketシステムコールの第1パラメータにAF_NETLINKを指定します。Netlinkを使っているユーザプロセスには、ipコマンドやdbusデーモン等があります。たとえば、ipコマンドを実行すると、socketシステムコールの第1パラメータにAF_NETLINKを指定していることがわかります。

[root@server ~]# strace -e trace=socket ip l
socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_ROUTE) = 3

なお、straceコマンドの使い方は、straceコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

2. 検証環境

CentOS版数は次のとおりです。

[root@server ~]# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)

カーネル版数は次のとおりです。

[root@server ~]# uname -r
3.10.0-1160.el7.x86_64

3 サンプルプログラムの作成

3.1 カーネルモジュールの作成

カーネルモジュールを作成します。カーネルジーュルは、カーネル空間で動作するプロセスです。ユーザ空間のプロセスが送信するメッセージを受信するカーネルモジュールを作成します。netlink_kernel_createの第2パラメータは、include/uapi/linux/netlink.hで未定義な値(17)を使いました。

[root@server ~]# cat netlink_module.c
#include <linux/module.h>
#include <linux/netlink.h>
#include <linux/init.h>
#include <net/sock.h>

#define NETLINK_TEST 17

static struct sock *nl_sock;
char *msg="kernel message";

static void nl_recv_msg (struct sk_buff *skb_in) {
    struct nlmsghdr *nlh;
    struct sk_buff *skb_out;
    int msg_size,pid;

    // receiving from user process
    nlh = (struct nlmsghdr *)skb_in->data;
    pid = nlh->nlmsg_pid;
    printk(KERN_INFO "received netlink message(%s) from PID(%d)\n", (char*)nlmsg_data(nlh), pid);

    // sending to user process
    msg_size=strlen(msg);
    skb_out = nlmsg_new(msg_size, 0);
    nlh=nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);
    NETLINK_CB(skb_out).dst_group = 0; /* not in mcast group */
    strncpy(nlmsg_data(nlh), msg, msg_size);
    nlmsg_unicast(nl_sock, skb_out, pid);
}

struct netlink_kernel_cfg cfg = {
    .input = nl_recv_msg,
};

static int __init netlink_module_init(void)
{
  printk(KERN_INFO "NETLINK TEST START\n");
  nl_sock = netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
  return 0;
}

static void __exit netlink_module_exit(void)
{
  printk(KERN_INFO "NETLINK TEST FINISH\n");
  netlink_kernel_release(nl_sock);
}

module_init(netlink_module_init);
module_exit(netlink_module_exit);

MODULE_AUTHOR("hana_shin");
MODULE_LICENSE("GPL v2");


次に、Makefileを作成します。$(MAKE)やrmの前はスペースではなくTABです。

[root@server ~]# cat Makefile
obj-m := netlink_module.o

KDIR    := /lib/modules/$(shell uname -r)/build
VERBOSE = 0

all:
        $(MAKE) -C $(KDIR) M=$(PWD) KBUILD_VERBOSE=$(VERBOSE) modules
clean:
        rm -f *.o *.ko *.mod.c Module.symvers modules.order

makeコマンドを実行して、カーネルモジュールを作成します。

[root@server ~]# make
make -C /lib/modules/3.10.0-1160.el7.x86_64/build M=/root KBUILD_VERBOSE=0 modules
make[1]: ディレクトリ `/usr/src/kernels/3.10.0-1160.el7.x86_64' に入ります
  CC [M]  /root/netlink_module.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/netlink_module.mod.o
  LD [M]  /root/netlink_module.ko
make[1]: ディレクトリ `/usr/src/kernels/3.10.0-1160.el7.x86_64' から出ます

作成したカーネルモジュールを確認します。netlink_module.koが作成されていることがわかります。

[root@server ~]# ls -l netlink_module.*
-rw-r--r--. 1 root root   1312  1月 10 22:27 netlink_module.c
-rw-r--r--. 1 root root 413968  1月 10 22:30 netlink_module.ko
-rw-r--r--. 1 root root   1337  1月 10 22:30 netlink_module.mod.c
-rw-r--r--. 1 root root  59384  1月 10 22:30 netlink_module.mod.o
-rw-r--r--. 1 root root 358480  1月 10 22:30 netlink_module.o

3.2 ユーザプログラムの作成

ユーザ空間で動作するプログラムを作成します。

[root@server ~]# cat netlink_user.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>

#define NETLINK_USER 17
#define MAX_PAYLOAD 1024

struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd;
struct msghdr msg;

int main(int argc, char *argv[])
{
    sock_fd=socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);

    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid();
    bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));

    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0;
    dest_addr.nl_groups = 0;

    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid();
    nlh->nlmsg_flags = 0;

    strcpy(NLMSG_DATA(nlh), argv[1]);

    iov.iov_base = (void *)nlh;
    iov.iov_len = nlh->nlmsg_len;
    msg.msg_name = (void *)&dest_addr;
    msg.msg_namelen = sizeof(dest_addr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    printf("My PID is %d\n", getpid());
    printf("Sending message to kernel: %s\n", argv[1]);

    /* Send message to kernel */
    sendmsg(sock_fd,&msg,0);
    printf("Waiting for message from kernel\n");

    /* Read message from kernel */
    recvmsg(sock_fd, &msg, 0);
    printf("Received message from kernel: %s\n", (char *)NLMSG_DATA(nlh));
    close(sock_fd);
    return 0;
}

コンパイルします。

[root@server ~]# gcc -Wall -o netlink_user netlink_user.c

実行ファイルを確認します。

[root@server ~]# ls netlink_user*
netlink_user  netlink_user.c

4 動作確認

サンプルプログラムが出力するログを確認するため、journalctlコマンドを実行します。

[root@server ~]# journalctl -f

カーネルモジュールをロードします。

[root@server ~]# insmod netlink_module.ko

ロードしたカーネルモジュールを確認します。作成したカーネルモジュールがロードされていることがわかります。

[root@server ~]# lsmod |grep netlink_module
netlink_module         12558  0

任意の文字列(11111)を指定してサンプルプログラム(netlink_user)を実行します。ユーザプロセスのPIDは4524であることがわかります。また、カーネルに"11111"を送信し、カーネルから"kernel message"というメッセージを受信していることがわかります。

[root@server ~]# ./netlink_user 11111
My PID is 4524
Sending message to kernel: 11111
Waiting for message from kernel
Received message from kernel: kernel message

カーネルモジュールが、PID=4524のユーザプロセスからメッセージを受信していることがわかります。

[root@server ~]# journalctl -f
-snip-
 1月 10 22:38:19 server kernel: NETLINK TEST START
 1月 10 22:39:47 server kernel: received netlink message(11111) from PID(4524)

後始末をします。netlink_moduleをアンロードします。

[root@server ~]# rmmod netlink_module

モジュールを確認します。netlink_moduleがアンロードされたことがわかります。

[root@server ~]# lsmod |grep netlink_module
[root@server ~]#

5 Netlinkメッセージ形式

ユーザプロセスからカーネルモジュールに送信される Netlinkメッセージを以下に示します。

                            dest_addr    sockaddr_nl
                               +----> +---------------+
                               |      | nl_family     | <= AF_NETLINKを設定する。
                               |      +---------------+
                               |      | nl_pad        |
                               |      +---------------+
             msghdr            |      | nl_pid        | <= プロセスのPID(getpid)を設定する。
msg -> +----------------+      |      +---------------+
       | *msg_name      | -----+      | nl_groups     | <= ユニキャスト通信なので0を設定する。
       +----------------+             +---------------+
       | msg_namelen(*1)|
       +----------------+                  iovec
       | *msg_iov       | ----------> +---------------+      nlh  |<- nlmsghdr ->|
       +----------------+             | *iov_base     | --------> +--------------+-----------------------+
       | msg_iovlen(*2) |             +---------------+           |              |                       |
       +----------------+             | iov_len(1024) | ----+     | 16(byte)     |                       | <= malloc()で獲得する。
       | msg_controllen |             +---------------+     |     |              |                       |
       +----------------+                                   |     +--------------+-----------------------+
       | msg_flags      |                                   |     |<------------ 1024(byte) ------------>|
       +----------------+                                   |                      A
                                                            |                      |
                                                            +----------------------+

(*1) sockaddr_nl構造体のサイズを設定する。
(*2) iovec構造体の個数(1個)を設定する。

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ