端末関連のことについて、自分のまとめになります。
大きな理解のミス等がないか、等ご確認ご指摘いただければ助かります。
些細なことでもコメントお願いいたします...
下記のページの画像(上から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等の端末上で描写されるテキストエディタプロセスの理解
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); } } } } } } } }
一個前でやったプログラムの画面入力を、SSHDが代行しているに他ならない!
更に上に、省略している入力側のターミナルエミュレータ周りのも追加すると...
単にシェルでssh 接続しただけなのに、
2組の擬似端末を直接や間接的に使うことになる。
もし、各擬似端末を双方向パイプに変えてしまうと、全く対話的ではなくなる。
擬似端末の凄さがわかる。
実際には、xサーバが最終的なディスプレイとキーボードの入出力を命令する。
xサーバとxクライアント(xterm)間はxプロトコルでやりとりする。
xサーバとxクライアントは1対Nの関係

あなたの回答
tips
プレビュー