今さら Linux Basics
POSIX (Portable Operating System Interface)
mmap()
mmap()
?
What is -
mmap()
は、呼び出しプロセスの仮想アドレス空間に、ファイルやデバイスをマッピングする。 -
prot
argument- メモリに対するプロテクションを指定する。
-
PROT_EXEC
: 実行可能 -
PROT_READ
: 読み込み可能 -
PROT_WRITE
: 書き込み可能 -
PROT_NONE
: アクセス不可
-
flags
argument- マッピングされた領域への書き込みが「他のプロセスにも見えるか」と「元となるファイルにも適用されるか」を決める。
-
MAP_SHARED
: マッピングされた領域への書き込みは、他のプロセスにも見える かつ 元のファイルにも適用される。 -
MAP_PRIVATE
: CoW (Copy-on-Write) を適用する。マッピングされた領域への書き込みは、他のプロセスには見えず かつ 元のファイルに適用されない。 -
MAP_ANONYMOUS
: 裏にファイルが存在しないマッピングを作成する。メモリの中身は 0 で初期化されている。
Huge Page
Address Translation
アドレス変換 (Address Translation) とは、プロセスがメモリ上のデータにアクセスする時に使うメモリアドレスを、物理メモリ上のメモリアドレスに変換することを言う。
x86 におけるメモリ管理機能は、セグメンテーション (Segmentation) とページング (Paging) で構成され、以下のようにメモリアドレスは変換される。
Logical Address =(Segmentation)=> Linear Address =(Paging)=> Physical Address
論理アドレス (Logical Address) は、セグメントセレクタ (Segment Selector) とセグメントオフセット (Offset) で構成される。
セグメントセレクタによって、グローバルセグメントテーブル (GDT: Global Descriptor Table) 上のエントリが指定され、そのエントリにセグメントディスクリプタ (Segment Descriptor) が格納されている。セグメントディスクリプタには、対象のセグメント (Segment) のベースアドレスが格納されている。リニアアドレス (Linear Address) は、(セグメントのベースアドレス) + (セグメントオフセット) で計算される。リニアアドレスは、ページングの設定に依存して、複数のレベルに分けられる。各レベルごとに OS がページテーブル (Page Table) を用意しており、与えられたリニアアドレスのそのレベルの部分が、そのテーブル上のインデックスとなり、ページテーブルエントリ (Page Table Entry) を取得できる。ページテーブルエントリは、次のレベルのページテーブルのベースアドレスを持っており、与えられたリニアアドレスのそのレベルの部分が、再びそのテーブルのインデックスになるという階層的な構造になっている。最終的に、ページのベースアドレスとリニアアドレスの最後のオフセットの部分を足したものが、物理アドレスになる。
Logical Address
(or Far Pointer)
+---------------------+
v v Linear Address
+------------------+ +------------------+ Linear Address +-----+-------+--------+ Physical Address
| Segment Selector | | Offset | Space +---------->| Dir | Table | Offset | Space
+--------+---------+ +---------+--------+ +----------------+ | +--+--+---+---+----+---+ +------------------+
| | | | | | | | | |
| | | | | | | +--------+ +- - - - - - - - - +<-+ Page
| Global Descriptor +--+ | | | | | Page Table | +------------------+ |
| Table (GDT) | +================+<-+ Segment | | | +-----------+ ++->| Physical Address | |
| +---------------+ | | | | | +-----------+ | | | | +------------------+ |
| | | | +----------------+<----+ Page | | Page Directory | +-----------+ | | | |
| +---------------+ | | | | | | | +------------+ ++->| Entry |-+-->+ - - - - - - - - -+<-+
| | Segment | | +- - - - - - - - + | | | | | | | +-----------+ | |
| | Descriptor +-+ | +---->| Linear Address +------------+ | +------------+ | | | | |
+->+---------------+ | +->| + - - - - - - - -+ | | +->| Entry |-+-->+-----------+ | |
| | | | +----------------+<----+ +------------+ | |
| | +------+---->+================+<-+ | | | |
+---------------+ Segment | | +------------+ | |
Base Address | | +------------------+
+----------------+
|------------------------- Segmentation ---------------------------|------------------------------- Paging ---------------------------------|
セグメンテーションは、64-bit モード (IA-32e Mode) では一般的に無効化されていて、フラットな 64-bit リニアアドレススペースになっており、64-bit の Linux では考えなくて良い。
Paging
ページングは、64-bit モード (IA-32e Mode) では、以下の2つのモードをとることができる。
- 4-Level Paging
- CR0.PG = 1、CR4.PAE = 1、IA32_EFER.LME = 1、CR4.LA57 = 0 のときに、このモードになる。
- 48-bit リニアアドレス (256 TB) が 52-bit 物理アドレス (4 PB) に変換される。
- 48-bit リニアアドレスを、4 つの 9 ビットと 12 ビットのオフセットに分ける。
- Bits 47:39 => Page Map Level 4 (PML4)
- Bits 38:30 => Page Directory Pointer
- Bits 29:21 => Page Directory
- Bits 20:12 => Page Table
- Bits 11:0 => Offset
- 5-Level Paging
- CR0.PG = 1、CR4.PAE = 1、IA32_EFER.LME = 1、CR4.LA57 = 1 のときに、このモードになる。
- 57-bit リニアアドレス (128 PB) が 52-bit 物理アドレス (4 PB) に変換される。
- Bits 56:48 => Page Map Level 5 (PML5)
- Bits 47:39 => Page Map Level 4 (PML4)
- Bits 38:30 => Page Directory Pointer
- Bits 29:21 => Page Directory
- Bits 20:12 => Page Table
- Bits 11:0 => Offset
ページングの利点としては、以下のようなものがある。
- 小さな利用可能なメモリを集めて、1つの連続したメモリかのように見せることができる。
- アドレス空間を分離することができる。つまり、あるプロセスが、別のプロセスに割り当てられているメモリに、アクセスできないように設定できる。
- 異なるアドレス空間が、同じ物理メモリを参照するように設定することもできる。
Translation Lookaside Buffers (TLBs)
上述の通り、ページングでは、リニアアドレスを複数の階層に分けて、物理アドレスに変換している。これをページウォーク (Page Walk) と言う。一方で、プロセスからメモリ上のデータにアクセスする度に、ページウォークが必要となることを意味している。ページテーブル自体もメモリ上に配置されているため、1回のページウォークで複数回のメモリアクセスが必要になる。CPU のクロック周波数に比べて、メモリのアクセスは非常に遅いことが一般的に知られている。これを高速化するために TLB が存在しており、リニアアドレスから物理アドレスへの変換に関する情報をキャッシュしている。
TLB には、ページ番号をページフレームにマップするエントリを含んでいる。用語は以下の通り。
- ページ番号 (Page Number): リニアアドレスの上位ビット (オフセットを除いた部分)
- ページフレーム (Page Frame): 物理アドレスの上位ビット (オフセットを除いた部分)
- ページオフセット (Page Offset): ページ内のオフセット
- ページサイズ (Page Size): ページオフセットのサイズ
Huge Page
上述のように、オフセットには 12 ビットが割り当てられているので、ページサイズは 4 KB になる。ただし、ページテーブルを使わないように設定することで、ページサイズを 2 MB にでき、さらにページディレクトリを使わないように設定することで、ページサイズを 1 GB にすることができる。このような設定を Huge Page と読んでいる。
TLB によって、リニアアドレスから物理アドレスへの変換は高速化できる一方で、TLB に保存できるマッピングの数 (エントリ数) にも上限が存在するため、TLB キャッシュが満杯になってしまった場合に、直近の変換を保存するために古い変換のエントリを削除する必要が出てくる。再び、その古い変換のアドレスにアクセスをした場合には、再びページウォークをする必要が出てきてしまう。Huge Page を使うことによって、1つエントリが含む物理アドレスのサイズが増加するため、同じ数の TLB キャッシュエントリでより多くの物理アドレスをカバーすることができるようになる。
Huge Page in Linux Kernel
Linux カーネルでは、Huge Page に関するインターフェースが大きく2つある。
- HugeTLB Pages (hugetlbpage)
- 最近のプロセッサがサポートしている複数のページサイズに則る。
-
mmap()
システムコールや hugetlbfs/libhugetlbfs を使って設定可能である。
- Transparent Huge Pages (THP)
- ページサイズを自動的に大きくしたり小さくしてくれる。
- sysfs インターフェースや
madvicse()
/prctl()
システムコールを使って設定可能である。
参考リンク
epoll API
- epoll API は、渡した複数のファイルディスクリプタのうち、いずれかが読み書き可能になったかどうかを監視するための API である。
- epoll API の中心となるものは epoll instance であり、ユーザースペース目線では、以下の2つリストのコンテナと考えることができる。
- interest list: 監視対象のファイルディスクリプタの集合
- ready list: 読み書きの準備ができているファイルディスクリプタの集合
- 以下のシステムコールを使って、epoll を作成・管理できる。
-
epoll_create()
/epoll_create1()
: 新しい epoll instance を作成し、その epoll instance への参照となるファイルディスクリプタを返す。 -
epoll_ctl()
: epoll instance の interest list に監視対象のエントリを登録したり、変更したり、削除したりする。 -
epoll_wait()
: interest list にある監視対象エントリに I/O イベントが発生するまで、呼び出し元スレッドをブロックする。
-
epoll_create()
/ epoll_create1()
- epoll instance を作成し、そのファイルディスクリプタを返す。
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
-
epoll_create()
- 元々は
size
引数は、呼び出し元 (caller) が追加する予定の epoll instance の数を、kernel に伝えるのに使われていた。kernel は、この情報をヒントとして使って、内部のデータ構造に必要なスペースを確保していた。現在は、このヒントなしでも、kernel は動的にサイズを変更できるようになっているが、後方互換性のために 0 より大きな値を渡すべきとされている。
- 元々は
-
epoll_create1()
-
size
引数をなくし、代わりにflags
を指定できるようになった。
-
epoll_ctl()
- epoll instance が保有する interest list のエントリを追加、変更、削除する。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *_Nullable event);
-
op
: 実行する操作の種類-
EPOLL_CTL_ADD
: interest list に新しいエントリを追加する。 -
EPOLL_CTL_MOD
: interest list 内のエントリの設定を変更する。 -
EPOLL_CTL_DEL
: interest list 内のエントリを削除する。
-
-
events
: 監視したいイベントのタイプのビットマスク + フラグ- イベントタイプ
-
EPOLLIN
:read()
可能な状態 -
EPOLLOUT
:write()
可能な状態 -
EPOLLRDHUP
: stream socket peer が接続を閉じた or 書き込み中に接続が閉じた状態 -
EPOLLPRI
: 例外的な状態 -
EPOLLERR
: エラーが発生した状態 -
EPOLLHUP
: ハングアップした状態
-
- フラグ
-
EPOLLET
: エッジトリガモード (デフォルトはレベルトリガモード) -
EPOLLONESHOT
: ワンショットモード (1 回イベントが発生したら無効化) -
EPOLLWAKEUP
: T.B.D. -
EPOLLEXCLUSIVE
: T.B.D.
-
- イベントタイプ
epoll_event
- epoll で監視したいイベントを表現するためのデータ型
- 監視したいイベントタイプを
events
にビットマップ形式で渡す。 -
data.fd
には監視対象のファイルディスクリプタを渡す。
#include <sys/epoll.h>
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
typedef union epoll_data epoll_data_t;
epoll_wait()
- epoll instance の監視対象のファイルディスクリプタでイベントが発生するのを待つ。
Hands-On
サーバー側の処理
- サーバーソケットの作成
- 11111 番ポートでリッスン
- 接続が来るのを epoll で監視
- 接続リクエストが来たら
- クライアントソケットをノンブロッキングに変更
- データが送られてくるのを epoll で監視
- クライアントソケットにデータが送られてきたら
- 送られてきたメッセージをエコーバック
クライアント側の処理
- 5 つソケットを作成
- 0 番目は、何もせずソケットを閉じる
- 1 番目は、短いメッセージを送って、返ってきたメッセージを表示
- 2 番目は、短いメッセージを送って、返ってきたメッセージを無視
- 3 番目は、サーバー側のバッファサイズより長いメッセージを送って、タイムアウト内に返ってきたメッセージを表示
- 4 番目は、ソケットを閉じすらしないで放置し、そのままプロセス終了
動作確認
サーバー側
$ ./server
=== evt #0 ===
new conection #5
=== evt #0 ===
new conection #6
=== evt #0 ===
new conection #7
=== evt #0 ===
new conection #8
=== evt #0 ===
new conection #9
=== evt #0 ===
connection closed by peer #5
=== evt #1 ===
echo #6: Hello, this is client #1.
=== evt #0 ===
echo #7: Hello, this is client #2.
=== evt #0 ===
echo #8: Hello, this is client #3. Sending
=== evt #0 ===
echo #8: a message longer than buffer siz
=== evt #0 ===
echo #8: e on the server side!
=== evt #0 ===
connection closed by peer #8
=== evt #1 ===
read(): Connection reset by peer
=== evt #2 ===
connection closed by peer #6
=== evt #3 ===
connection closed by peer #9
クライアント側
$ python3 client.py
Got a message on client #1: b'Hello, this is client #1.'
Got a message on client #3: b'Hello, this is client #3. Sending'
Got a message on client #3: b' a message longer than buffer siz'
Got a message on client #3: b'e on the server side!'
ソースコード
サーバー側
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SERV_PORT 11111
#define BACKLOG 10
#define MAXEVTS 10
#define BUFSIZE 32
void die(const char *msg)
{
perror(msg);
exit(EXIT_FAILURE);
}
int setup_listener_socket() {
int sock, ret, optval;
struct sockaddr_in addr;
// create a socket.
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock < 0)
die("socket()");
// allow to reuse port.
optval = 1;
ret = setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
if (ret < 0) {
close(sock);
die("setsockopt()");
}
// construct a socket address.
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(SERV_PORT);
// bind the address to the socket.
ret = bind(sock, (struct sockaddr *)&addr, sizeof(addr));
if (ret < 0) {
close(sock);
die("bind()");
}
// listen on the socket.
ret = listen(sock, BACKLOG);
if (ret < 0) {
close(sock);
die("listen()");
}
return sock;
}
int get_client_socket(int listener) {
int sock, flags;
socklen_t len;
struct sockaddr_in addr;
// accept a new connection.
sock = accept(listener, (struct sockaddr *)&addr, &len);
if (sock < 0) {
perror("accept()");
return -1;
}
// set non-blocking
flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
return sock;
}
int main()
{
int epollfd, listener, i, nfds, client, rd, wr, sent;
struct epoll_event evt, evts[MAXEVTS];
char buffer[BUFSIZE + 1];
// create an epoll instance.
epollfd = epoll_create1(0);
if (epollfd < 0)
die("epoll_create1()");
// set up a listener socket.
listener = setup_listener_socket();
// register 'read' event for the listener socket.
memset(&evt, 0, sizeof(evt));
// evt.events = EPOLLIN | EPOLLET;
evt.events = EPOLLIN;
evt.data.fd = listener;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listener, &evt);
for (;;) {
// wait for events available on the epoll instance.
nfds = epoll_wait(epollfd, evts, MAXEVTS, -1);
for (i = 0; i < nfds; i++) {
printf("=== evt #%d ===\n", i);
if (evts[i].data.fd == listener) {
// new connection requested.
client = get_client_socket(listener);
if (client < 0)
continue;
printf("new conection #%d\n", client);
// register 'read' event for the client socket.
memset(&evt, 0, sizeof(evt));
evt.events = EPOLLIN;
evt.data.fd = client;
epoll_ctl(epollfd, EPOLL_CTL_ADD, client, &evt);
} else {
// message received on client sockets
client = evts[i].data.fd;
rd = read(client, buffer, sizeof(buffer));
if (rd > 0) {
sent = 0;
do {
wr = write(client, buffer + sent, rd - sent);
if (wr < 0) {
perror("write()");
epoll_ctl(epollfd, EPOLL_CTL_DEL, client, &evt);
close(client);
break;
}
sent += wr;
} while (sent != rd);
buffer[sent] = '\0';
printf("echo #%d: %s\n", client, buffer);
} else {
if (rd == 0)
printf("connection closed by peer #%d\n", client);
else
perror("read()");
epoll_ctl(epollfd, EPOLL_CTL_DEL, client, &evt);
close(client);
}
}
}
}
return 0;
}
クライアント側
import time
import socket
HOST = "localhost"
PORT = 11111
BUFSIZE = 4096
N = 5
socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM) for i in range(N)]
for sock in socks:
sock.connect((HOST, PORT))
sock.settimeout(0.1)
time.sleep(1)
# client 0 just closes the socket.
socks[0].close()
# client 1 sends a message and receives a response.
msg = "Hello, this is client #1."
socks[1].sendall(msg.encode("utf-8"))
data = socks[1].recv(BUFSIZE)
print(f"Got a message on client #1: {data}")
# client 2 sends a message but ignores a response.
msg = "Hello, this is client #2."
socks[2].sendall(msg.encode("utf-8"))
# client 3 sends a message longer than the buffer size on the server size.
msg = "Hello, this is client #3. Sending a message longer than buffer size on the server side!"
socks[3].sendall(msg.encode("utf-8"))
while True:
try:
data = socks[3].recv(BUFSIZE)
except socket.timeout:
break
print(f"Got a message on client #3: {data}")
# client 4 does nothing.
time.sleep(1)
参考リンク
fsync() / sync()
fsync()
#include <unistd.h>
int fsync(int fd);
- ファイルに対して行った全ての変更をディスクデバイスにフラッシュする。
sync()
#include <unistd.h>
void sync(void);
- メタデータへの変更やキャッシュされてるファイルデータを、ファイルシステムに書き込む。
参考リンク
memfd
- メモリ上にのみ存在する無名ファイル (anonymous file)
- 永続ストレージ (persistent storage) 上のファイルには存在しない
- 無名メモリ (anonymous memory) が利用される
Hands-On in C
動作確認
$ ./a.out
Memfd content: Hello, memfd!
main.c
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
int main() {
int ret, memfd;
const char *content = "Hello, memfd!";
size_t length = strlen(content) + 1;
char *map;
// Create an anonymous memory file.
memfd = memfd_create("memfd", 0);
if (memfd < 0) {
perror("memfd_create");
return 1;
}
// Resize the memory file.
ret = ftruncate(memfd, length);
if (ret == -1) {
perror("ftruncate");
close(memfd);
return 1;
}
// Write content to the memory file.
ret = write(memfd, content, length);
if (ret != length) {
perror("write");
close(memfd);
return 1;
}
// Map the memory file into memory.
map = mmap(NULL, length, PROT_READ, MAP_PRIVATE, memfd, 0);
if (map == MAP_FAILED) {
perror("mmap");
close(memfd);
return 1;
}
// print the content of the memory file.
printf("Memfd content: %s\n", map);
// Clean up.
munmap(map, length);
close(memfd);
return 0;
}
Hands-On in Python
動作確認
$ python3 main.py
b'Hello, memfd!'
main.py
import os
memfd = os.memfd_create("memfd")
data = b"Hello, memfd!"
os.write(memfd, data)
os.lseek(memfd, 0, os.SEEK_SET)
size = len(data)
print(os.read(memfd, size))
os.close(memfd)
Process / Process Group / Session
- プロセス (Process): Linux におけるプログラムの実行の基本単位である。各プロセスには一意なプロセス ID が割り当てられる。
- プロセスグループ (Process Group): 1つ以上のプロセスの集合のことである。各プロセスグループには一意なプロセスグループ ID (PGID) が割り当てられる。
- セッション (Session): 1つ以上のプロセスグループの集合のことである。各セッションには一意なセッション ID (SID) が割り当てられる。
制御ターミナルとセッション
- セッションリーダー (Session Leader):
setsid()
を呼び出すと、新しいセッションが作成され、それを呼び出したプロセスがセッションリーダーになる。セッション ID は、プロセス ID と同じになる。 - 制御ターミナル (Controlling Terminal): セッションは制御ターミナルを持つ場合があり、セッションリーダーがセッションと制御ターミナルの紐付けを確立する。これにより、セッション内のプロセスにユーザーの入力を行ったり、プロセスの出力を確認することができる。
- 制御ターミナルからの独立: 新しいセッションを作成した場合に、その新しいセッションは既存の制御ターミナルから切り離される。これはデーモンプロセスにおいて重要である。制御ターミナルからログアウトした場合、セッションリーダーに SIGHUP シグナルが送信され、そのセッションのプロセスを終了する。デーモンプロセスは常駐プログラムであるため、制御ターミナルから切り離す必要がある。
Hands-On
動作確認
$ ./a.out
Parent process, pid = 114657
Child process, pid = 114658
New session created, pid = 114658
$ sudo tail -f /var/log/messages
Jan 29 21:32:28 ip-172-31-70-218 daemon-example[114658]: Hello from daemon
Jan 29 21:32:31 ip-172-31-70-218 daemon-example[114658]: Hello from daemon
Jan 29 21:32:34 ip-172-31-70-218 daemon-example[114658]: Hello from daemon
Jan 29 21:32:37 ip-172-31-70-218 daemon-example[114658]: Hello from daemon
Jan 29 21:32:40 ip-172-31-70-218 daemon-example[114658]: Hello from daemon
Jan 29 21:32:43 ip-172-31-70-218 daemon-example[114658]: Hello from daemon
^C
$ sudo kill -9 `pidof ./a.out`
main.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <syslog.h>
#include <stdbool.h>
int main() {
pid_t pid;
/* Fork the current process */
pid = fork();
if (pid < 0) {
/* Error occurred */
perror("fork");
return -1;
} else if (pid > 0) {
/* Parent process */
printf("Parent process, pid = %d\n", getpid());
return 0; // Parent exits
} else {
/* Child process */
printf("Child process, pid = %d\n", getpid());
/* Create a new session */
if (setsid() < 0) {
perror("setsid");
return -1;
}
printf("New session created, pid = %d\n", getpid());
/* Open the syslog for logging */
openlog("daemon-example", LOG_PID, LOG_DAEMON);
while (true) {
/* Write a log message */
syslog(LOG_INFO, "Hello from daemon");
/* Sleep for 3 seconds */
sleep(3);
}
/* Close the syslog (not reached in this example) */
closelog();
return 0;
}
}