この記事はLinuxアドベントカレンダー20日目の記事です!
書いたきっかけ
UNIXネットワークプログラミング〈Vol.1〉を読んでいてTCPサーバの種類を読んでいたのですが中でもファイルディスクリプタパッシングなアーキテクチャのものってあまりない?と思い気になって調べてみたので書きました
ディスクリプタパッシングとは
ディスクリプタパッシングは ファイル記述子をUnixドメインソケット経由で親子関係にない別プロセスへ転送する手法です。
やってみる
以下のサンプルコードを実行するとfork(2)の後にオープンしたfdを親から子へわたし、子がreadして内容を読み取って表示できていることがわかります。ここでのポイントは単純なread/writeでfdの数値を渡すだけではダメでsendmsg(2)/recvmsg(2) の補助データ枠を使ってプロセス間で通信する必要があります。
#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/wait.h> #include <time.h> #include <unistd.h> /* sendmsg(2)を使ってfdを送信する */ static void wyslij(int socket, int fd) { struct msghdr msg = { 0 }; char buf[CMSG_SPACE(sizeof(fd))]; memset(buf, '\0', sizeof(buf)); struct iovec io = { .iov_base = "ABC", .iov_len = 3 }; msg.msg_iov = &io; msg.msg_iovlen = 1; msg.msg_control = buf; msg.msg_controllen = sizeof(buf); struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg); // cmsghdrへのポインターを返す cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; // これが肝でファイル記述子のセットを送受信するためのタイプ cmsg->cmsg_len = CMSG_LEN(sizeof(fd)); *((int *) CMSG_DATA(cmsg)) = fd; // cmsghdrのデータ部分へのポインターを返す msg.msg_controllen = CMSG_SPACE(sizeof(fd)); if (sendmsg(socket, &msg, 0) < 0) printf("Failed to send message\n"); } /* recvmsg(2)を使ってfdを受信する */ static int odbierz(int socket) { struct msghdr msg = {0}; char m_buffer[256]; struct iovec io = { .iov_base = m_buffer, .iov_len = sizeof(m_buffer) }; msg.msg_iov = &io; msg.msg_iovlen = 1; char c_buffer[256]; msg.msg_control = c_buffer; msg.msg_controllen = sizeof(c_buffer); if (recvmsg(socket, &msg, 0) < 0) printf("Failed to receive message\n"); struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg); // cmsghdrへのポインターを返す unsigned char * data = CMSG_DATA(cmsg); // cmsghdrのデータ部分へのポインターを返す int fd = *((int*) data); // fdを取得 printf("Extracted fd %d\n", fd); return fd; } int main(int argc, char **argv) { const char *filename = "test.txt"; int sv[2]; // socketpair(2) 接続されたソケットのペアを作成する if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) != 0) printf("Failed to create Unix-domain socket pair\n"); int pid = fork(); if (pid > 0) { // 親プロセス printf("Parent-> pid: %d\n", getpid()); close(sv[1]); int fd = open(filename, O_RDONLY); // 子プロセスへUnixドメインソケット経由でfdを渡す wyslij(sv[0], fd); sleep(1); } else { // 子プロセス printf("Child-> pid: %d, ppid: %d\n", getpid(), getppid()); close(sv[0]); sleep(1); int fd = odbierz(sv[1]); printf("Read %d!\n", fd); char buffer[256]; ssize_t nbytes; while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0) write(1, buffer, nbytes); close(fd); } return 0; }
技術的な仕組み
を参考に読んでいくと技術的にはオープンしたファイルテーブルのエントリへのポインタをあるプロセスから別のプロセスへ渡すことで実現されているとのこと。単純な数値の受け渡しではないのがポイントです。ちなみに送り元である親プロセスがfdをcloseしても子プロセスのfdはクローズされないようでした。
コード内コメントにあるSCM_RIGHTSの詳細は以下に記載されていました
SCM_RIGHTS 他のプロセスでオープンされたファイルディスクリプタのセットを送受信する。 データ部分 にファイルディスクリプタの整数配列が入っている。 渡されたファイルディスクリプタ は、あたかも dup(2) で生成されたかのように振る舞う。
まとめ
何が嬉しいのかはbkブログでの言及があるようなセキュリティの場面だったりが多いのでしょうか?(具体的な利用例はあまり見たことないので詳しい方教えて欲しいです!!)