質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Linux

Linuxは、Unixをベースにして開発されたオペレーティングシステムです。日本では「リナックス」と呼ばれています。 主にWebサーバやDNSサーバ、イントラネットなどのサーバ用OSとして利用されています。 上位500のスーパーコンピュータの90%以上はLinuxを使用しています。 携帯端末用のプラットフォームAndroidは、Linuxカーネル上に構築されています。

Q&A

0回答

1001閲覧

端末/疑似端末関連理解確認

akiyama3284pga

総合スコア186

Linux

Linuxは、Unixをベースにして開発されたオペレーティングシステムです。日本では「リナックス」と呼ばれています。 主にWebサーバやDNSサーバ、イントラネットなどのサーバ用OSとして利用されています。 上位500のスーパーコンピュータの90%以上はLinuxを使用しています。 携帯端末用のプラットフォームAndroidは、Linuxカーネル上に構築されています。

0グッド

2クリップ

投稿2022/12/09 01:35

端末関連のことについて、自分のまとめになります。
大きな理解のミス等がないか、等ご確認ご指摘いただければ助かります。
些細なことでもコメントお願いいたします...
下記のページの画像(上から2枚)を使用させて頂きました。
http://www.linusakesson.net/programming/tty/
とても長くなってしまって申し訳ありません。
また、コードも自環境での動作(ubuntu)しか確認できておりません。


GUI(X)環境では、エミュレータ(gnorm端末等)をユーザ空間に出すことで、エミュレータの柔軟性を出すようになっている。

イメージ説明
from: http://www.linusakesson.net/programming/tty/

イメージ説明
from: http://www.linusakesson.net/programming/tty/

なぜ、ptyのmaster側とslave側がという2つがGUI環境では必要なのか?

それは、上のようにカーネルの仕事を切り出したため、その両端のプロセス(シェルとgnorm端末等)がお互いにやりとりする際に挟む必要があるカーネルの端末をソフトウェア的に実現するための機能である端末(tty)ドライバや、Line dicipline等の機能を利用するために、それぞれがカーネルのそれら機能にアクセスするための入り口として設けられる必要があるため(これらデバイスファイルはシステムコール的な存在)。

もし、ターミナルエミュレータとシェル等が直接繋がっているだけなら、端末として成立しない。

なぜなら、ターミナルエミュレータは単にキーボードと画面の表現機能を受け持つだけ(実際には加工[文字への色付け、clear、カーソル移動等]もやる)であり、その表示する"もの"は全てカーネル内の端末ドライバ等から貰うから。

例えばカノニカルの場合、プロセスから読み取りを行うと、"端末ドライバ"が入力された行を返す。これをターミナルエミュレータが単加工なりして表示する。

Line diciplineがenter(return)されるまで蓄えつつ入力された文字を1文字1文字返すことでターミナルエミュレータがこれを表示(これがエコーバック!!入力された文字が都度表示されるのは当然ではなくこのような仕組みがあるから、パスワード入力時とかに表示されないのはエコーバックを切っているから)、
E
C
H
O

H
L
L
O

Enter!ここでようやく、line diciplineが端末ドライバへその行を渡し、ドライバが読み取り(scanf(), input()...)したプロセスへその行を渡す。(行区切りを見ると読み取りから復帰する。)

行終わりのサインとしては、NL/EOL/EOL2/EOFが用いられるが、内EOFだけは端末ドライバ内で破棄されるが、他は最後の文字として呼出し元に返される。

図にある通り、slave側に端末ドライバの機能があるため、これは仮想コンソールと似た構図となる。(シェル等側から見てslave側デバイスファイルが制御端末の役割を担う。)

GUI環境で端末アイコンをクリックすると、
Gnorm端末プロセスからbashが起動され、bashの制御端末はgnorm端末プロセスがopenした疑似端末のslave側となる。

イメージ説明

正確にはguiでのttyドライバは疑似端末ドライバ

英語では、
Pseudo Terminal疑似端末 (/dev/pty)
Controlling Terminal制御端末 (/dev/tty)
なので、GUI環境の疑似端末は制御端末ではないが、機能としては制御端末とほぼ同じなのでそう言ってしまっても差し支えない。

  • Vim等の端末上で描写されるテキストエディタプロセスの理解

1.ウィンドウサイズの調整連携
イメージ説明

2.なぜすぐにプロセス(vim)に文字が出るのか
非カノニカルモードに、vimが制御端末を使用時だけ変更することで、line diciplineで文字が行分蓄える作業がスルーされるため、1文字1文字がすぐに押される度にプロセスへと渡されるため。
イメージ説明

Vim等のテキストエディタは、ひたすらにこのように、1文字ずつを

受け取り、それを独自のスクリーンに表示するというセットをループで繰り返し、

保存時に実際にファイルに反映するプログラムだと言える。

課題:端末・疑似ドライバとlinuxコンソール・ターミナルエミュレータの関連性(仕事分担)がどうなっているのか大まかに理解するにはどうすればいいか。

LOAD1

PTYとPIPEの違いを勉強することで、PTYの行っている特殊なことが浮き彫りにする。

大まかに、

PIPE

単純な一方方向のみのデータチャネル("a"を入力したら一方から"a"が出てくる。)

PTY

両方向から読み書き可能なデータチャネルということにプラスして端末を表現するデータにエンコードしたり、逆にプロセス(シェル等)が理解できるようにデコードしたりしてくれる。

単にaを書き込みしてaという情報だけをterminal emulator等が手にしたとしても、それを端末として表現する情報(termios構造体に詰まっている)を一緒に渡したり管理したり、端末上で動くプロセスに対してシグナルを送ったり、起点となる特殊文字をキャッチして処理したり、行末までバッファリングしたりと色々とすることがあり(そうでないと端末として成立しない)、それをpty(疑似端末)が行ってくれる。(かなり多くの処理が挟まっている。ので単純なパイプとはこの点が異なる。)

LOAD2
linuxコンソールについての深い記事は中々無いが、ターミナルエミュレータについては様々な企業や個人で開発がされているユーザ主導のプログラムなので、多くの参考に出会えた。
結局、大まかな理解を進めるには、自分で作るないしはソースを見るのが一番だと思い下記のような記事を参考にしてみて、その本質を理解しようと思った。
https://www.uninformativ.de/blog/postings/2018-02-24/0/POSTING-en.html
すると自分の何となくこうなっているんだろうな、という憶測の重要な部分は大体合っていた。

記事中
There are some things that you don’t have to implement. On the other hand, the pseudoterminal driver is ignorant of any escape sequences. And this is where the fun starts.
printf '\033[1mThis is bold text.\n'・・・

疑似端末ドライバ(==slave)からは、「033[1mThis is bold text.」をこの幅で表示せよ、みたいに送られて来る。つまり、エスケープシーケンスの対応(この場合文字装飾)等の味付けをターミナルエミュレータは任されているということ...
味付け(clear等含む)が必要無いのであればターミナルエミュレータの存在意義は無い(わざわざユーザランドに出して好きにしていいよ、としている意味がない。)
仮想コンソールではこの味付けをlinuxコンソールが行っているので、同様の033[1m...を送れば、きちんと装飾される。(その味付けに対応していれば)
slave側プロセス(シェル等)からは疑似端末がほぼ完全に制御端末として利用できるということ。(エコーのオンオフ、カノニカル・非カノニカルのオンオフ等端末固有の機能が使える。)
Master側は単にslave側とのデータの送受信許可をしたりするだけのもので、"端末"ではない。

世の有名なターミナルエミュレータは、VT1000という実端末をエミュレートしているソフトウェア。
ソフトウェアということは、
VT1000などが実際の映像化やキーボードの入出力を実装しないといけないのに対して、
キーボードやディスプレイはすでに当linux機につながっているものを使用するだけなので、
LinuxのVGAドライバやkeybordドライバを借りるだけでいいということ。(それはxサーバが)
疑似端末を実際に自分のプログラムで扱うには、
疑似端末APIを使用するだけでよくなっている。
posix_openpt(),grantpt(),unlockpt(),ptsname()等...

  • 実際に疑似端末を使用するプログラムを書き、どのような連携が行われるのかを知る。

Step1. マスタ作成->fork()->スレーブ作成 の流れでやりとり前までを形作る。
Bsd系osでない場合はopen()するだけで条件が揃っていれば制御端末になるそう。

この時点で下の図のような感じになる。
イメージ説明

Step2. 疑似端末を使う

単純なrawモードを使用する。(エコーや行制御などらしいことは全くしない、ただのパイプのよう...)

上の図を使うと、下のようになる。
イメージ説明

Step3. 制御端末らしさを体感する。
もし、slave側でpython等のプロセスを起動して、それを使うことができたら、それはslave側プロセスのセッション及び制御端末の機能が使えているということ。
正にこの際のslave側の状況は、ターミナルエミュレータやsshd/telned... 等の多くの会話をメインとするインタラクティブなプロセスの本質が理解できる。
さきのプログラムではslave側は単にmasterから来たのをそのまま返しただけだったのを、
そこで他のプログラムを実行するということ。slave側からfork()やexec()等されたプロセスの制御端末はslave(疑似端末)となるので、実行されたプロセスからmaster側に送りたければ、単にprintf()等してやればいいだけ。つまりpython等の中身を変えなくてもいいということ。

端末作成、利用2.c

#define _XOPEN_SOURCE 600 #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <unistd.h> #include <stdio.h> #define __USE_BSD #include <termios.h> #include <string.h> #include <sys/ioctl.h> #include <sys/select.h> // 追加! #include <sys/types.h> int ptym_open(){ int fdm, fds, rc; // fdm => master side's file desc, fds => slave side's file desc fdm = posix_openpt(O_RDWR); rc = grantpt(fdm); rc = unlockpt(fdm); return fdm; } int ptys_open(int fdm){ int fds; fds = open(ptsname(fdm), O_RDWR); return fds; } pid_t make_ptys_and_ptym_finally_pty_fork(int* ptrfdm, int argc, char* argv[]){ int fdm, fds; pid_t pid; struct termios slave_orig_term_settings; // 現設定用(初期値) struct termios new_term_settings; // カスタム設定用 struct winsize wiize; // 端末のウィンドウサイズ用 fdm = ptym_open(); pid = fork(); if (pid == 0){ setsid(); fds = ptys_open(fdm); close(fdm); tcgetattr(fds, &slave_orig_term_settings); new_term_settings = slave_orig_term_settings; cfmakeraw(&new_term_settings); // 初期値のcookedをrawモードに tcsetattr(fds, TCSANOW, &new_term_settings); // 反映(TCSANNOW==即時) close(0); close(1); close(2); dup(fds); dup(fds); dup(fds); close(fds); // 変更! (もう、制御端末となっているのでfdsは不要) // ioctl(0, TIOCSCTTY, 1); // 不要 /* -----------------------------------------------*/ /* 疑似端末の利用!(slave) */ /* -----------------------------------------------*/ // 以下プログラム実行コード追加! int rc; // 普通にターミナルから引数を入れてプロセス起動したかのようにするため、邪魔な // 実行する対象引数(python...)を消したものをslave側の引数とする。 // ./test python -args ==> python -args (これをchild_argsとする。) // ★ コマンド引数で渡された実行するものをslave側で実行 // これにより、現在のslave側プロセスが、実行するプロセスになるが、制御端末等は当然受け継ぐので // そのアプリでの標準出力はマスタ側へ向かい、マスタ側の標準出力(ディスプレイ)に出力されるし、 // そのアプリでの標準入力はマスタ側からのものとなる。 rc = execvp(argv[1], argv[2]); }else{ /* 親側 */ close(fds); *ptrfdm = fdm; return pid; } } int main(int argc, char* argv[]){ // 引数確認 if (argc <= 1){ fprintf(stderr, "Usage: %s program_name [parameters]\n", argv[0]); exit(1); } int ptrfdm; pid_t slave_side_pid; char input[150]; char input2[150]; int rc; // マスタ・スレーブ作成、fork() オールインワン関数 slave_side_pid = make_ptys_and_ptym_finally_pty_fork(&ptrfdm, argc, argv); /* -----------------------------------------------*/ /* 疑似端末の利用!(親 == master側) */ /* -----------------------------------------------*/ // 親子両方がこのコードを通るとまずいので if (getpid() != slave_side_pid){ fd_set fd_in; // 変更! while (1){ char *pad; // 標準入力と疑似端末マスタ側を監視(動きあるまでblocking...) // FD_ZERO(&fd_in); // 変更! FD_SET(0, &fd_in); // 変更! FD_SET(ptrfdm, &fd_in); // 変更 ! rc = select(ptrfdm + 1, &fd_in, NULL, NULL, NULL); // 変更! // 以下switch変更! // 動きがあったのを特定 switch(rc){ case -1 : fprintf(stderr, "select()でエラー発生! %d\n", errno); exit(1); default: { // 標準入力にデータが来た if (FD_ISSET(0, &fd_in)){ rc = read(0, input, sizeof(input)); if (rc > 0){ // マスタ側に書き込む == スレーブ側に送る。 write(ptrfdm, input, rc); }else{ if (rc < 0){ fprintf(stderr, "read(標準入力)でエラー発生!%d\n", errno); exit(1); } } } // master側疑似端末にデータが来た if (FD_ISSET(ptrfdm, &fd_in)){ rc = read(ptrfdm, input, sizeof(input)); if (rc > 0){ // 標準出力に書き出す。 write(1, input, rc); }else{ if (rc < 0){ fprintf(stderr, "read(master)でエラー発生! %d\n", errno); exit(1); } } } } } } } }

大体以下のような具合で実行される。
イメージ説明

SSHが上の"応用"だということを図で示したい。
イメージ説明

一個前でやったプログラムの画面入力を、SSHDが代行しているに他ならない!
更に上に、省略している入力側のターミナルエミュレータ周りのも追加すると...
イメージ説明
単にシェルでssh 接続しただけなのに、
2組の擬似端末を直接や間接的に使うことになる。
もし、各擬似端末を双方向パイプに変えてしまうと、全く対話的ではなくなる。
擬似端末の凄さがわかる。
実際には、xサーバが最終的なディスプレイとキーボードの入出力を命令する。
xサーバとxクライアント(xterm)間はxプロトコルでやりとりする。
xサーバとxクライアントは1対Nの関係

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Zuishin

2022/12/09 01:45

> 些細なことでもコメントお願いいたします... サイトの使い方を間違っていませんか?
68user

2022/12/09 07:22

この理解で正しいのか知りたいという質問なので、何ら問題ないんじゃないですかね。 > Master側は単にslave側とのデータの送受信許可をしたりするだけのもので、"端末"ではない。 正解を知らないんで単なる疑問なんですが、エスケープシーケンスを読んで termcap や terminfo を参照しあるべき挙動を行うのって master 側端末じゃないんですかね (違う?)
68user

2022/12/09 07:52

最近知ってびっくりしたので誰にとはなく単に言ってみたいだけなんですが、htop とか vim などで端末上でマウス操作ができるんですね (htop で画面下部の F1 とか F2 をクリックしたり、vim でクリックした位置にカーソル移動したり)。よくわかってないんですが readline の mouse tracking の機能なんでしょうか。
Zuishin

2022/12/09 08:07

https://teratail.com/help/avoid-asking > 何か困っている理由があり、最適解を見つける為の知見を得たい場合は、その理由や何が知りたいのかを明確に記述してください。 > 悪い例:「オススメの開発環境構築方法を教えてください」 > 良い例:「Dockerで開発環境を構築していて、ファイルの書き込みができません」 これを見ると、何ら問題がないと言えないと思います。 解決すべき明確な課題が無い場合には、このように「問題・課題が含まれていない質問」という編集依頼がつきます。 解決すべき問題はなんでしょうか? 少なくとも些細なコメントを募集するような使い方は想定されていません。
akiyama3284pga

2022/12/09 08:08

ありがとうございます。 もしうまくまとまりそうであれば、キータの意見交換などにも掲載してみようかと思います。 以前自分が質問した際のotn様のご回答がわかりやすいのではないかと思います。 スレーブ側(シェルやシェルから起動したプロセス)が擬似端末を通して繋がるターミナルエミュレータ(xterm、gnorm端末等)がそれぞれに決めているエスケープシーケンスをDBから読んできて対応のものを送って、受けた動作を付与するような具合かと思います。 つまり、参照するのはスレーブ側で、その注文を受けて実際アクションするのはマスター側だと思います… https://teratail.com/questions/9y1fxlxfb68bps マウス等に関してはまだ未開拓でございます。申し訳ございません
Zuishin

2022/12/09 08:16

マルチポストをすると言っているように読めます。
jimbe

2022/12/13 02:12 編集

>この理解で正しいのか知りたいという質問 >>自分のまとめになります と明言されてれていますので、質問では無いのでしょう。 そも「理解が正しいかという質問」が「プログラミングで問題になっている」のかがとても疑問です。 「こうしたくてこう書いたがこうなってしまった→この仕様はこういう意味じゃないのか→こういう理解で正しいのか」という流れなら分かりますが、これまでの質問を見ても単に勉強しているだけに見えます。 > もしうまくまとまりそうであれば、キータの意見交換などにも掲載してみようかと思います。 こっちでやらずにそっちでやってくださいという内容です。 teratail を自身の作業場か何かと勘違いしていませんか。発表する場でも添削して貰う場でもないはずですが。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだ回答がついていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問