目次: Linux
前回(2025年12月18日の日記参照)はpreadとlseek + readの違いを見るため、テストプログラムを作って動作確認しました。今回はLinuxカーネルの実装の違いを見ます。
カーネルバージョンは6.9です。最近のバージョンであればさほど実装は変わらないと思います。
// linux/fs/read_write.c
SYSCALL_DEFINE4(pread64, unsigned int, fd, char __user *, buf,
size_t, count, loff_t, pos)
{
return ksys_pread64(fd, buf, count, pos);
}
ssize_t ksys_pread64(unsigned int fd, char __user *buf, size_t count,
loff_t pos)
{
if (pos < 0)
return -EINVAL;
CLASS(fd, f)(fd);
if (fd_empty(f))
return -EBADF;
if (fd_file(f)->f_mode & FMODE_PREAD)
return vfs_read(fd_file(f), buf, count, &pos);
return -ESPIPE;
}
まずはpreadの実装です。pread64システムコールはksys_pread64()を呼び出します。ksys_pread64()はvfs_read()にバッファ、サイズ、オフセット(= ファイル位置)のパラメータをそのまま渡します。ファイルディスクリプタが一切登場しません。シンプルですね。vfs_read()の先は基本的に各ドライバのread実装になります。
次はlseek+readの実装です。lseekから見ましょう。
// linux/fs/read_write.c
SYSCALL_DEFINE3(lseek, unsigned int, fd, off_t, offset, unsigned int, whence)
{
return ksys_lseek(fd, offset, whence);
}
static off_t ksys_lseek(unsigned int fd, off_t offset, unsigned int whence)
{
off_t retval;
CLASS(fd_pos, f)(fd);
if (fd_empty(f))
return -EBADF;
retval = -EINVAL;
if (whence <= SEEK_MAX) {
loff_t res = vfs_llseek(fd_file(f), offset, whence);
retval = res;
if (res != (loff_t)retval)
retval = -EOVERFLOW; /* LFS: should only happen on 32 bit platforms */
}
return retval;
}
loff_t default_llseek(struct file *file, loff_t offset, int whence)
{
struct inode *inode = file_inode(file);
loff_t retval;
retval = inode_lock_killable(inode);
if (retval)
return retval;
switch (whence) {
case SEEK_END:
offset += i_size_read(inode);
break;
case SEEK_CUR:
if (offset == 0) {
retval = file->f_pos;
goto out;
}
offset += file->f_pos;
break;
case SEEK_DATA:
/*
* In the generic case the entire file is data, so as
* long as offset isn't at the end of the file then the
* offset is data.
*/
if (offset >= inode->i_size) {
retval = -ENXIO;
goto out;
}
break;
case SEEK_HOLE:
/*
* There is a virtual hole at the end of the file, so
* as long as offset isn't i_size or larger, return
* i_size.
*/
if (offset >= inode->i_size) {
retval = -ENXIO;
goto out;
}
offset = inode->i_size;
break;
}
retval = -EINVAL;
if (offset >= 0 || unsigned_offsets(file)) {
if (offset != file->f_pos)
file->f_pos = offset;
retval = offset;
}
out:
inode_unlock(inode);
return retval;
}
EXPORT_SYMBOL(default_llseek);
先ほど同様にlseekシステムコールはksys_lseek()を呼び、ksys_lseek()はvfs_llseek()を呼ぶだけです。vfs_llseek()の先は各ドライバの実装になります。特にこだわりがない場合は通常のlseekの振る舞いをするdefault_llseek()を使うことが多いので、default_llseek()の実装を載せておきました。
素直にwhenceの値に従ってoffsetを計算(例: SEEK_SETの場合はoffsetの値をそのまま使う、SEEK_CURの場合は現在のfile->f_posを足す)し、ファイルディスクリプタのメンバ(file->f_pos)に設定しています。
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
CLASS(fd_pos, f)(fd);
ssize_t ret = -EBADF;
if (!fd_empty(f)) {
loff_t pos, *ppos = file_ppos(fd_file(f));
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_read(fd_file(f), buf, count, ppos);
if (ret >= 0 && ppos)
fd_file(f)->f_pos = pos;
}
return ret;
}
最後にreadの実装です。readシステムコールはksys_read()を呼び出しており、ksys_read()はファイルディスクリプタからオフセットを取得(fd_file(f)->f_pos)して、vfs_read()にバッファ、サイズ、オフセットのパラメータを渡します。
オフセットをどこから取ってくるかが違います。
もしlseekが実装されていないファイルの場合、lseekはオフセットをファイルディスクリプタに設定できません。readはファイルディスクリプタを参照してreadの実装に渡すので、オフセットは0スタート(= ファイル先頭から読み出し)です。
一方のpreadはファイルディスクリプタを無視してシステムコールに渡されたオフセットをそのままreadの実装に渡すので、lseekがあろうがなかろうが任意のオフセットから読み出せます。
この記事にコメントする
目次: Linux
知っている人には「なんだそんなことか」で終わりな話なんですが、お恥ずかしいことに私は今まで知らなかったのでメモしておきます。
Linuxには指定されたオフセットからデータを読み出すシステムコールpread(pread64)があります。私は今までpread = lseek + readだと思っていましたが、特定の環境だと両者の動作が違います。
まずは確認のためのテストプログラムを用意しました。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
int fd = -1;
char buf[256];
ssize_t nr;
off_t pos, offset1 = 0x1000, offset2 = 0x2000;
fd = open("/dev/sample_char/sample_char_dev", O_RDWR);
if (fd == -1) {
perror("open");
goto err_out;
}
// lseek + read
pos = lseek(fd, offset1, SEEK_SET);
if (pos == (off_t)-1) {
perror("lseek 1");
}
nr = read(fd, buf, sizeof(buf));
if (nr != (ssize_t)sizeof(buf)) {
perror("read 1-1");
}
nr = read(fd, buf, sizeof(buf));
if (nr != (ssize_t)sizeof(buf)) {
perror("read 1-2");
}
pos = lseek(fd, offset2, SEEK_SET);
if (pos == (off_t)-1) {
perror("lseek 2");
}
nr = read(fd, buf, sizeof(buf));
if (nr != (ssize_t)sizeof(buf)) {
perror("read 2-1");
}
nr = read(fd, buf, sizeof(buf));
if (nr != (ssize_t)sizeof(buf)) {
perror("read 2-2");
}
// pread
nr = pread(fd, buf, sizeof(buf), offset1);
if (nr != (ssize_t)sizeof(buf)) {
perror("pread 1");
}
nr = pread(fd, buf, sizeof(buf), offset2);
if (nr != (ssize_t)sizeof(buf)) {
perror("pread 2");
}
err_out:
if (fd != -1) {
close(fd);
fd = -1;
}
return 0;
}
やっていることは、
こんな操作です。
ファイルにどんな操作が行われたか知るために、キャラクタデバイスを作成してseekやreadのログを出します。方法は別にキャラクタデバイスでなくても何でも良いんですが、Linuxカーネルモジュールでキャラクタデバイスを実装するのが楽だと思います。ソースコードはGitHubに置きました。
sudo apt-get install linux-headers-amd64 make sudo insmod sample-char-dev.ko gcc test.c -o test ./test
カーネルモジュールのビルド方法とテストプログラムの実行方法は上記のとおりです。
特にカーネルモジュールの実装を変えなければ、キャラクタデバイスのファイル位置はlseekで変更可能かつreadにより進む実装になります。テストプログラムを実行すると下記のようなカーネルログが出るはずです。カーネルログはsudo dmesg -wなどで見てください。
sample_char: open. sample_char: seek to 1000. sample_char: read 256 bytes at 1000. ★lseek + read: 位置1000から読み出せる★ sample_char: read 256 bytes at 1256. sample_char: seek to 2000. sample_char: read 256 bytes at 2000. ★lseek + read: 位置2000から読み出せる★ sample_char: read 256 bytes at 2256. sample_char: read 256 bytes at 1000. ★pread: lseek(1000)+readと一緒★ sample_char: read 256 bytes at 2000. ★pread: lseek(2000)+readと一緒★ sample_char: release.
シングルスレッドかつlseek可能な通常のファイルではlseek + readでも、preadでも結果は同じです。位置1000もしくは位置2000からreadできます。
しかしlseekだけできない特殊なファイルだと結果が異なります。先ほどのキャラクタデバイスのlseekを無効にして試しましょう。
static const struct file_operations sample_char_fops = {
.owner = THIS_MODULE,
.open = sample_char_fops_open,
.release = sample_char_fops_release,
.read = sample_char_fops_read,
.write = sample_char_fops_write,
// .llseek = sample_char_fops_llseek, ★この行をコメントアウトする★
};
変更によりlseekだけ失敗し、readは成功するファイル(テストプログラムがIllegal seekのエラー表示を出す)になりました。この状態でテストプログラムを実行すると下記のようなカーネルログが出るはずです。カーネルログはsudo dmesg -wなどで見てください。
sample_char: open. sample_char: read 256 bytes at 0. ★lseek + read: 位置1000から読み出しているつもりが0からになる★ sample_char: read 256 bytes at 256. sample_char: read 256 bytes at 512. ★lseek + read: 位置2000から読み出しているつもりが512からになる★ sample_char: read 256 bytes at 768. sample_char: read 256 bytes at 1000. ★pread: 変わらず位置1000から読み出せる★ sample_char: read 256 bytes at 2000. ★pread: 変わらず位置2000から読み出せる★ sample_char: release.
最初はlseek + readのログですが、位置1000もしくは2000からreadしたいのに、lseekが失敗するため意図しない位置を読み出してしまう一方、preadはlseekと無関係に指定された位置1000もしくは2000から読み出せます。
個人的にはファイル位置に意味があるのにlseekできないファイルを作るのは良くない実装だと思いますが、Linuxカーネルにもこのような実装が存在しています。ちょうど最近出会ったvfioがlseek無効の実装になっていました。vfioはファイル位置によってデバイスのどのメモリ領域を読み出せるか変わります。例えばvfio-pciであればBAR 0はこの位置、BAR 1はこの位置、みたいに決まっています。
// linux/drivers/vfio/vfio_main.c
const struct file_operations vfio_device_fops = {
.owner = THIS_MODULE,
.open = vfio_device_fops_cdev_open,
.release = vfio_device_fops_release,
.read = vfio_device_fops_read,
.write = vfio_device_fops_write,
.unlocked_ioctl = vfio_device_fops_unl_ioctl,
.compat_ioctl = compat_ptr_ioctl,
.mmap = vfio_device_fops_mmap,
};
しかし上記のようにvfioにはllseekの定義がありません。lseek+readではなくpreadで読み出さないと正常に動作しないことに気づくまでかなり困惑しました。こういう実装は良くないと思うんだ……。
この記事にコメントする
目次: 射的
アキバのターゲット1にて減圧レギュレータ(0.4MPa)とホース、コネクタを買いました。いわゆるエアガンを外部から供給するガスで動作させるための部品「外部ソースキット」と呼ばれるものの一部です。
ガスガンを作動させるには何らかの圧縮ガスが必要で、ざっくりいってこの3パターンがあります。
| 動作方式 | 動作ガス | 外部ソースキット | 空き缶 | デメリット |
|---|---|---|---|---|
| フロン | 液化フロンガス | 不要 | 発生する | 冷えに弱い、速く動かすと液化ガスが噴き出る |
| 外部ソース | CO2ボンベ | 必要 | 発生する | 必要な道具が多くやや高い |
| エアーコンプレッサー | 圧縮空気 | 必要 | 発生しない | ホースが必要、コンプレッサーの近くでしか使えない |
私はずっとフロンガスだったので、エアガンの動作用フロンガスの空き缶が家にどんどん溜まります。空き缶は500ccのPETボトルくらいあって割と邪魔で、なんとかならないかなーと思っていました。
CO2外部ソースは知っていたものの空き缶が発生することに変わりはなくて、フロンガスの空き缶がCO2ガスの空き缶に代わっても嬉しくないので購入を見送っていました。
最近になって、アキバのターゲット1にエアーコンプレッサーが設置されたため(しかも今のところは無料で使わせていただける)、これが決め手になって外部ソースキットの一部購入に踏み切りました。家にフロンガスの空き缶が増えるのが防げるのはかなりありがたいです。
この記事にコメントする
目次: Linux
Ubuntu 24.04 LTSで起動中にカーネルパニックを起こすとこんな感じの画面になります。画面にはエラーメッセージの最後と思われる1行のみ表示され、他のカーネル起動ログは隠されます。
エラーの原因がわかりにくくて開発の邪魔なので無効にするべく調べました。どうやらカーネルの設定らしくてCONFIG_DRM_PANICを有効/無効にするとこの画面が出る/出ないが切り替わります。
Device Drivers --->
Graphics support --->
<M> Direct Rendering Manager (XFree86 4.1.0 and higher DRI support) --->
[*] Display a user-friendly message when a kernel panic occurs
(0xffffff) Drm panic screen foreground color, in RGB
(0x5e2750) Drm panic screen background color, in RGB
Ubuntu 24.04 LTSと同じ配色にしたい場合は、上記のような設定にすると良いです。何がユーザーフレンドリーかわかんないし、私はこの機能は要らんです……。
この記事にコメントする
目次: Linux
LXDEにはLXPanelといってタスクバーやスタートメニューなどを表示しているアプリがいます。LXPanelのタスクバーにはドラッグするとボタンを入れ替えられる機能がありますが、LXPanelのボタン入れ替え機能はバグっているようで、たびたび動かなくなります。起動直後に発生率高い気がします。
ボタン入れ替え機能が死んでしまったときは、LXPanelの何もないところを右クリックして[パネルの設定]を選び、設定ウインドウを表示させましょう。
下記のように操作すると直ることが多いです。
私の環境(Debian Testing, forky/sid)だと、1回でダメでも何回かやったら直ることもあります。どうやっても直らないときもありますから、その時は諦めて再起動ですね……。
この記事にコメントする
| < | 2025 | > | ||||
| << | < | 12 | > | >> | ||
| 日 | 月 | 火 | 水 | 木 | 金 | 土 |
| - | 1 | 2 | 3 | 4 | 5 | 6 |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 | - | - | - |
23年4月10日
25年12月19日
25年12月8日
25年12月18日
25年12月11日
22年4月13日
16年1月25日
16年1月8日
13年8月11日
23年9月11日
08年9月10日
08年9月11日
22年8月16日
22年8月22日
22年3月18日
25年11月30日
25年12月14日
25年11月28日
25年11月29日
23年5月15日
wiki
Linux JM
Java API
2002年
2003年
2004年
2005年
2006年
2007年
2008年
2009年
2010年
2011年
2012年
2013年
2014年
2015年
2016年
2017年
2018年
2019年
2020年
2021年
2022年
2023年
2024年
2025年
過去日記について
アクセス統計
サーバ一覧
サイトの情報合計:
本日: