Open
17

Linux勉强メモ

この本を参考に。
ある程度シェルとか扱っていることに慣れていると、これはとてもわかりやすく感じる

Linuxのカーネル、プロセス、ストリーム、ファイルシステム、システムコールなどの理解がざっくりだった為、
gccプログラミングを通じてその辺りの理解を深めたい

https://www.amazon.co.jp/dp/B075ST51Y5/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

環境を用意する

Dockerfileを用意した。

# build
docker build -t bob:v1 .

# Run
docker run -it --rm --mount type=bind,source=`pwd`,target=/app bob:v1 bash

https://github.com/jnuank/linux_gcc/blob/master/Dockerfile
# CentOSのイメージ取得
$ docker pull centos:7

# 名前付けてコンテナ作成
$ docker run -it -d --name linuxpg centos:7

# コンテナの中に入る
$ docker exec -it linuxpg bash

コンテナ入ってgccコマンドが使えるようにする

$ yum update

# 1.2章を参考にしたが、若干コマンド違った。CentOS7系だから?
$ yum groupinstall -y 'Development Tools'

# バージョン確認
$ gcc -dumpversion
4.8.5

manコマンドを使えるようにする

Dockerで立てたCentOSにはmanコマンドが入っていない。

tsflags=nodocs となっているのでインストールしてくれないっぽい

$ cat /etc/yum.conf
[main]
cachedir=/var/cache/yum/$basearch/$releasever
keepcache=0
debuglevel=2
logfile=/var/log/yum.log
exactarch=1
obsoletes=1
gpgcheck=1
plugins=1
installonly_limit=5
bugtracker_url=http://bugs.centos.org/set_project.php?project_id=23&ref=http://bugs.centos.org/bug_report_page.php?category=yum
distroverpkg=centos-release
override_install_langs=en_US.utf8
tsflags=nodocs


$ yum -y install man-pages
$ yum -y install man

$ manpath
/usr/local/share/man:/usr/share/man

# 再度、設定フラグを付けてインストール
$ yum --setopt=tsflags='' reinstall man-pages

gccでのビルド

Segmentation fault は、NULLポインタ参照したり、配列の範囲を大きく超えてアクセスしたり。

int main(int argc, char *argv[])

  • int argc には、コマンドラインに入力した引数の数
  • char *argv[] には、コマンドラインに入力した引数の値
    • 注意すべきは、コマンド名や実行したファイルも引数の1つとなる

デバイスファイル

Linuxではデータを保持する物以外もファイルとして扱う。
デバイス(HDD、SSD、キーボード、ディスプレイ)もファイルとして扱う。
 → デバイスファイル

すごい抽象化。

[root@372ea366ea04 hello]# ll /dev/
total 0
crw--w---- 1 root tty  136, 0 Jan 15 12:34 console
lrwxrwxrwx 1 root root     11 Jan 15 12:34 core -> /proc/kcore
lrwxrwxrwx 1 root root     13 Jan 15 12:34 fd -> /proc/self/fd
crw-rw-rw- 1 root root   1, 7 Jan 15 12:34 full
drwxrwxrwt 2 root root     40 Jan 15 12:34 mqueue
crw-rw-rw- 1 root root   1, 3 Jan 15 12:34 null
lrwxrwxrwx 1 root root      8 Jan 15 12:34 ptmx -> pts/ptmx
drwxr-xr-x 2 root root      0 Jan 15 12:34 pts
crw-rw-rw- 1 root root   1, 8 Jan 15 12:34 random
drwxrwxrwt 2 root root     40 Jan 15 12:34 shm
lrwxrwxrwx 1 root root     15 Jan 15 12:34 stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root     15 Jan 15 12:34 stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root     15 Jan 15 12:34 stdout -> /proc/self/fd/1
crw-rw-rw- 1 root root   5, 0 Jan 15 13:58 tty
crw-rw-rw- 1 root root   1, 9 Jan 15 12:34 urandom
crw-rw-rw- 1 root root   1, 5 Jan 15 12:34 zero

/dev/random は乱数を生成してくれる。
cat /dev/random をやると、延々とデータが吐き出される。

/dev/urandom も乱数発生だが、過去の乱数シードを使い回すっぽい?

/dev/null はデータを捨ててくれるゴミ箱的存在

プロセス

プロセスは、動作中のプログラムのこと。
プログラムは、ファイルのような形態で存在している、データのことも含む言葉

catコマンドはプログラム?
catコマンドを叩いて実行しようとしているのがプロセス?

コマンドプロンプトも、プロセス?

ターミナルを開いて、入力を待ち受けているやつもプロセス。

# 現在のプロセスID確認
$ echo $$
12400

# 起動中プロセスの確認
$ ps auxwef
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root     12400  0.0  0.0  11908  1776 pts/1    Ss   01:24   0:00 bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/
root     13129  0.0  0.1  51740  3408 pts/1    R+   06:54   0:00  \_ ps auxwef HOSTNAME=372ea366ea04 TERM=xterm LS_COLORS=rs=0:di=01
root         1  0.0  0.0  11840   668 pts/0    Ss+  00:48   0:00 /bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/s

TTY :端末

psコマンドで出たTTYは、このプロセスがどの端末に繋がっているかを示してる。
TeleTYpe(テレタイプ)の略

ファイルディスクリプタ

ファイルディスクリプタは、プロセスの接続先を示している(一番初めに挙げた本だと、ストリームと言っている)

各プロセスにファイルディスクリプタが設定されており、0が標準入力、1が標準出力、2が標準エラー出力は基本存在している。

/proc/${プロセスID}/fd を見ると、そのプロセスが使えるファイルディスクリプタを確認できる。
以下は、0〜2が同じ /dev/pts/1 という仮想端末に繋がっている。

$ ll /proc/$$/fd
total 0
lrwx------ 1 root root 64 Jan 16 06:45 0 -> /dev/pts/1
lrwx------ 1 root root 64 Jan 16 06:45 1 -> /dev/pts/1
lrwx------ 1 root root 64 Jan 16 06:45 2 -> /dev/pts/1
lrwx------ 1 root root 64 Jan 16 06:45 255 -> /dev/pts/1

参考記事

https://qiita.com/toshihirock/items/22de12f99b5c40365369

標準入力を受け取り、標準出力として出すような、catコマンドみたいなものをCで再現しようとしたときに、
read関数で0〜2どれを選んでも同じ挙動となっていた。
それは、0〜2が同じ仮想端末に繋がっているから問題が無かったといえる。

for (;;) {
        n = read(STDIN_FILENO, buf, sizeof buf);
        if (n == 0) break;
        write(STDOUT_FILENO, buf, n);
}

パイプで繋がっている状況を確認する

プロセスごとにファイルディスクリプタに示されている接続先が違うのであれば、
パイプはどうなっているのか。

試す

Aターミナル、Bターミナルと用意

Bターミナル

# 入力が来るまで待ち状態
 $ cat | cat

Aターミナル

$ ps auxw
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  11840  2752 pts/0    Ss+  01:38   0:00 /bin/bash
root        16  0.0  0.0  11840  2936 pts/1    Ss   01:39   0:00 bash
root        31  0.0  0.0  11840  2852 pts/2    Ss   01:39   0:00 bash
root        45  0.0  0.0   4396   680 pts/2    S+   01:39   0:00 cat
root        46  0.0  0.0   4396   660 pts/2    S+   01:39   0:00 cat
root        51  0.0  0.1  51744  3180 pts/1    R+   01:43   0:00 ps auxw

cat のプロセスの1つが、標準出力がpipe:[111261] に繋がり、
もう1つのプロセスの標準入力にpipe:[111261]に切り替わっていることがわかる

$ ll /proc/{45,46}/fd
/proc/45/fd:
total 0
lrwx------ 1 root root 64 Jan 18 01:40 0 -> /dev/pts/2
l-wx------ 1 root root 64 Jan 18 01:40 1 -> pipe:[111261]
lrwx------ 1 root root 64 Jan 18 01:39 2 -> /dev/pts/2

/proc/46/fd:
total 0
lr-x------ 1 root root 64 Jan 18 01:40 0 -> pipe:[111261]
lrwx------ 1 root root 64 Jan 18 01:40 1 -> /dev/pts/2
lrwx------ 1 root root 64 Jan 18 01:39 2 -> /dev/pts/2

FILE型

ファイルディスクリプタに、バッファ機能を付けたもの。ラッパー。

それらを扱った入出力で fopen fclose fgetc fputc などがある

stdioのライブラリはちゃんとバッファリングモードが利いているか

straceコマンドをインストール

yum -y install strace

データの準備

空文字で4096バイト分のファイルを用意

dd bs=4096 if=/dev/zero count=1 of=data

read、writeのシステムコール版

bufサイズは2048をファイル内で指定

# strace -e trace=open,read,write,close ./cat data > /dev/null
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`&\2\0\0\0\0\0"..., 832) = 832
close(3)                                = 0
open("data", O_RDONLY)                  = 3
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 2048) = 2048
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 2048) = 2048
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 2048) = 2048
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 2048) = 2048
read(3, "", 2048)                       = 0
close(3)                                = 0
+++ exited with 0 +++

stdio版

一回のreadとwriteのシステムコールで終了している。

# strace -e trace=open,read,write,close ./catstd data > /dev/null
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`&\2\0\0\0\0\0"..., 832) = 832
close(3)                                = 0
open("data", O_RDONLY)                  = 3
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096
read(3, "", 4096)                       = 0
close(3)                                = 0
write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096
+++ exited with 0 +++

fgetc() って何を返してくれている?

       #include <stdio.h>

       int fgetc(FILE *stream);

1バイト読み込み、その数値を返してくれている。
中身は何をやっているのか。確認をしてみる。

・・・
                f = fopen(argv[i], "r");
                while ((c = fgetc(f)) != EOF){
                        printf("%d\n", c);
                        if(putchar(c) < 0) exit(1);
                        printf("\n");
                }
・・・

c = fgetc(f) で1バイトずつファイル終端に来るまで読み込みながら、putcharで読み込んだバイトを出力している。
そのc自体が何を来るのか確認してみる。

テストデータ

# cat data2
alice
bob
cathy

実行

# ./catstd data2 | head
97
a
108
l
105
i
99
c
101
e

各文字とそれに対応する文字コードが表示されている。
プログラム上では1バイトずつ読み込んで、書き出すことをやっているけど、
背後のシステムコールは1度のwriteとreadでやっているみたい

stdioのライブラリのfgetcとfputcharを使った場合、バッファリングは4096っぽいけど、setvbuf で変更はできる。

第7章 headコマンドを作る

Linuxにおいて「行」とは「\nで終わる文字列」

末尾 \0 ってなんだっけ?

headコマンドを作る時に、fgets(3) ではなく、 getc(3) を使う理由

char *fgets(char *buf, int size, FILE *stream);

fgets(3) の仕様上、1行読み込むか、バッファいっぱいまで書き込んで止まったのかが区別ができない。

※ただ、ネットワークプログラミングなどでは、行長の制限が逆に有効になる場合もあり

オプションの慣習について

パラメータを取らないオプションと、パラメータを取るオプションで大別ができる。

  • ls -a
  • head -n 5

パラメータを取らないオプションは、 ls -als のように、1つに纏められる。
パラメータありのオプションは、 head -n5 のように、オプションとパラメータをくっつけることも可能。

ハイフンが2つあるのはロングオプションと言われ、
ハイフンが1つのものはショートプションとなっている。

psコマンドでは、BSDスタイルという、オプションにハイフンがつかないスタイルも存在している。

https://qiita.com/shukan0728/items/f91fee7c0c740ce95df8

mktempでユニークな一時ファイル、一時ディレクトリが作れるっぽい

[root@a5d4c09a13d7 linux]# ROOTFS=$(mktemp -d)
[root@a5d4c09a13d7 linux]# echo $ROOTFS
/tmp/tmp.20Jxja5FQj

nvmeファイルシステムとは

Filesystemで /dev/nvme とは何かと思った。

# df
Filesystem     1K-blocks     Used Available Use% Mounted on
overlay        490691512 23054456 442641576   5% /
tmpfs              65536        0     65536   0% /dev
tmpfs            8014824        0   8014824   0% /sys/fs/cgroup
shm                65536        0     65536   0% /dev/shm
/dev/nvme0n1p2 490691512 23054456 442641576   5% /app
tmpfs            8014824        0   8014824   0% /proc/asound
tmpfs            8014824        0   8014824   0% /proc/acpi
tmpfs            8014824        0   8014824   0% /proc/scsi
tmpfs            8014824        0   8014824   0% /sys/firmware

SSDにアクセスするための仕様っぽい?

https://www.dell.com/support/kbdoc/ja-jp/000137207/rhel7上のnvme

標準入力とコマンドライン引数によるファイルディスクリプタの違い

たとえばファイルの中身を見るlessコマンドを使う時に、二通りのやり方ができる

less cat.c
less < cat.c (もしくは、cat cat.c | less

動作的に違いは全く無いが、ファイルディスクリプタを見ると違いが出てくるのがわかる。

標準入力で渡す

$ less  < app/cat.c
$ ll /proc/196794/fd
total 0
lr-x------ 1 root root 64 Mar 23 02:49 0 -> /app/cat.c
lrwx------ 1 root root 64 Mar 23 02:49 1 -> /dev/pts/1
lrwx------ 1 root root 64 Mar 23 02:49 2 -> /dev/pts/1
lr-x------ 1 root root 64 Mar 23 02:49 3 -> /dev/tty

これは予想通りの動き

コマンドライン引数で渡す

$ less app/cat.c
# 別端末で確認
$ ll /proc/196788/fd
total 0
lrwx------ 1 root root 64 Mar 23 02:47 0 -> /dev/pts/1
lrwx------ 1 root root 64 Mar 23 02:47 1 -> /dev/pts/1
lrwx------ 1 root root 64 Mar 23 02:47 2 -> /dev/pts/1
lr-x------ 1 root root 64 Mar 23 02:47 3 -> /dev/tty
lr-x------ 1 root root 64 Mar 23 02:47 4 -> /app/cat.c

コマンドライン引数で渡した時に、引数がファイルディスクリプタの4番で渡っている。

試しにvimでファイルをコマンドライン引数に渡して開いてみる

vi app/cat.c
# 別端末で確認
$ ll /proc/196798/fd
total 0
lrwx------ 1 root root 64 Mar 23 02:51 0 -> /dev/pts/1
lrwx------ 1 root root 64 Mar 23 02:51 1 -> /dev/pts/1
lrwx------ 1 root root 64 Mar 23 02:50 2 -> /dev/pts/1
lrwx------ 1 root root 64 Mar 23 02:51 4 -> /app/.cat.c.swp

コマンドライン引数などは、もしかしたら4番以降に入るとか、あるのかもしれない?

ログインするとコメントできます