unixにおけるファイルとユーザ管理
Unix演習サーバへの接続
Unix(Linux)は、インターネットでのサーバとして広く活用されている。Linuxを試すには、Windows ならば WSL や Cygwin であったり、Mac でも使える仮想OSの VMware, VirrtualBox を使うこともできる。今回の演習では、全員が同じ環境で使うために、クラウド環境にサーバを準備し利用する。
ネットワークの向こう側にあるサーバを利用する場合、以下のような方法が使われる。
- telnet (port 23)
- キー入力を相手に送って、送られてくるデータを画面に表示する。
- 通信データが暗号化されないので盗聴される心配があり、一般的には使用しない。
- rsh (remote shell – port 514)
- ネットワークを越えてコマンドを実行したりファイル転送ができる。
- telnet 同様に暗号化されていないので、次に示す ssh を使うのが一般的。
- ssh (secure shell – port 22)
- rsh の処理を暗号化しながら実行。
- ネットワークを越えた処理を行う際の基本だが、ssh を経由した攻撃が多いことから、通常のポート番号22以外を使ったり、アクセス制限を厳しく設定する必要がある。
- remote Desktop
- ネットワークの先のPCの画面をネットワーク越しに触れるようにしたもの。
教室のWiFi環境(fnct-student)では、HTTP(80) , HTTPS(443) の通信しか使えないことから、ssh(22) が通常利用できない。電子情報のWiFiアクセスポイント(nitfc-ei-student等)であれば、ssh などが使用できる。
今回授業の演習では、さくらインターネットのサーバ上のクラウドサーバを利用する。
ただし、さくらインターネットのクラウドサーバでは、ssh(port=22)が使用できるが、ssh 接続の際にログインパスワードの間違いなどが多発すると、ssh 経由の攻撃の可能性があると判断され、ssh(port=22)接続が一定時間使えなくなる対策がとられている。今回は、ゲストアカウントでパスワード入力ミスが多発することが想定されるので、port=22のsshは使用しない。
リモート接続を行う
Windows 10 or Windows 11 ならば、cmd.exe , macOS ならば、ターミナルソフトを起動し、以下の操作を行う。
$ ssh -p 443 ゲストID@演習サーバ
- 443ポートは通常は https 用だが、今回はサーバで ssh プロトコルを 443 ポートで受け付けるように設定してある。かなり特殊な使い方なので要注意。
- 演習サーバの接続方法(学内のみ) – サーバへの攻撃を極力へらすために非公開。
- 今回の演習では、センターIDではなくゲストIDを使います。
- ゲストIDのパスワードは、こちらのファイル(Teams)を参照。(2025-4EI Teams)
- パスワード入力時は、打つたびに●●●といった文字は表示されません。
- パスワード入力時にタイプミスした時は、Ctrl-U で最初から入力のやり直しができます。
ファイル操作の基本
まずは基本操作をしてみよう。ls コマンド(list) は、ディレクトリ内にあるファイルの一覧を表示する。cat コマンド(catalog)は、指定されたファイルの内容を表示する。
s53599xx@nitfcei:~$ ls helloworld.c Maildir public_data public_html s53599xx@nitfcei:~$ ls -l total 8 -rw-r--r-- 1 s53599xx students 76 Dec 21 14:30 helloworld.c drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir (略) s53599xx@nitfcei:~$ cat helloworld.c #include <stdio.h> int main() { printf( "Hello World\n" ) ; return 0 ; } s53599xx@nitfcei:~$
ファイルをコピーするには cp コマンド(copy)、不要なファイルを消すには rm コマンド(remove)を使う。
s53599xx@nitfcei:~$ cp helloworld.c test.c s53599xx@nitfcei:~$ ls -l total 8 -rw-r--r-- 1 s53599xx students 76 Dec 21 14:30 helloworld.c drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir -rw-r--r-- 1 s53599xx students 76 Dec 21 14:40 test.c (略) s53599xx@nitfcei:~$ rm test.c s53599xx@nitfcei:~$ ls -l total 8 -rw-r--r-- 1 s53599xx students 76 Dec 21 14:30 helloworld.c drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir s53599xx@nitfcei:~$
ファイル詳細表示の説明
ls -l で表示される詳細の内容は以下の通り。
属性 | リンク数 | 所有者 | グループ | サイズ | 日付 | ファイル名 |
---|---|---|---|---|---|---|
– rw- r– r– | 1 | s53599xx | students | 76 | Dec 21 14:30 | helloworld.c |
d rwx — — | 5 | s53599xx | students | 4096 | Dec 21 14:30 | Maildir |
– | d | -: 通常ファイル, d:ディレクトリ | ||||
rw- | r,w,x | 所有者が r:読み出し, w:書き込み, -: 権限なし ファイルなら、x:実行可能 ディレクトリなら、x:ディレクトリに入れる |
||||
r – – | – – – | グループの rwx の属性 r– は 読み込みだけ許可 | ||||
r – – | – – – | その他の rwx の属性 — は、読み書き禁止 |
基本的なファイル操作コマンド一覧
操作 | Linux | Windows |
---|---|---|
ディレクトリ一覧(list) ディレクトリ詳細 |
ls 場所 ※ ls -l 場所 |
dir /w 場所 ※ dir 場所 |
※ 省略時はカレントディレクトリ | ||
ファイル表示(catalog) | cat 場所 | type 場所 |
ファイルコピー(copy) | cp コピー元 コピー先 cp コピー元 コピー先ディレクトリ |
copy コピー元 コピー先 |
ファイル削除(remove) | rm 場所 | del 場所 |
ディレクトリ作成(make dir) | mkdir 場所 | md 場所 |
ディレクトリ削除(remove dir) | rmdir 場所 | rmdir 場所 |
カレントディレクトリ移動 (change directory) |
cd 場所 | cd 場所 ドライブの場合は ドライブ名: |
所有者を変更(change owner) | chown 所有者 場所 | |
グループを変更(change group) | chgrp グループ 場所 | |
属性を変更(change mode) | chmod 属性 場所 | ←属性の書き方 |
ワイルドカード文字
ls などのコマンドで、複数のファイルを対象とするとき、ワイルドカード文字が使える。
任意の1文字 ? |
(例) $ ls # 全部のファイル aaa.c ab.c abc.c bcd.c defgh.c hij.cxx $ ls a?.c # aで始まる2文字のC言語ファイル ab.c $ ls ???.c # 3文字のC言語のファイル aaa.c abc.c bcd.c |
任意の文字 * |
(例) $ ls a*.c # aで始まるC言語ファイル aaa.c ab.c abc.c $ ls *.cxx # 拡張子が.cxxのファイル(C++) hij.cxx |
相対PATHと絶対PATH
ファイルの場所を指定するには、2つの方法がある。
絶対PATHは、木構造の根(ルートディレクトリ / で表す) からの経路のディレクトリ名を”/”で区切って書き連ねる。ルートディレクトリからの場所であることを示すために、先頭を / で始める。住所を /福井県/越前市/宮谷町/斉藤家 と書くようなもの。
相対PATHは、現在注目しているディレクトリ(カレントディレクトリと呼ぶ)からの経路を書く。住所でいうと、/福井県/越前市 に注目している状態で、宮谷町/斉藤家 と書くようなもの。
ただし、/福井県/福井市 に注目している状態で、片町/山本家 は1つのファイルでも、/福井県/福井市/片町/山本家 とは別に /石川県/金沢市/片町/山本家 があるかもしれない。
上記の絵であれば、/home/tsaitoh/helloworld.c を、相対PATHで書く場合、s53599xx の一つ上にさかのぼって場所を指定することもできる。一つ上のディレクトリ(親ディレクトリ)は .. (ピリオド2つ)
この場合、” $ cat ../tsaitoh/helloworld.c ” の様な相対PATHでもアクセスできる。
カレントディレクトリ自身を表す場合は、. (ピリオド1つ)を使う。
/home/s53599xx/helloworld.c の場所は、” $ cat ./helloworld.c ” と書くこともできる。
ユーザとグループ
unixでは、ユーザとグループでアクセス制限をすることができる。ユーザ情報は、/etc/passwd ファイルで確認できる。グループ情報は、/etc/group ファイルで確認できる。
$ more /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin (略) guest00:x:1200:1200:guest00,,,:/home0/guests/guest00:/bin/bash $ more /etc/group root:x:0: daemon:x:1: bin:x:2: (略) guests:x:1200:guest00,guest01,guest02,...
/etc/passwd | /etc/group |
guest00 — ユーザID x — 昔は暗号化されたパスワード 1200 — ユーザID番号 1200 — グループID番号(/etc/groupを参照) guest00,,, — ユーザの正式名や電話番号など /home0/guests/guest00 — ホームディレクトリ /bin/bash — 使用する shell |
guests — グループID x — 昔は暗号化されたグループパスワード 1200 — グループID番号 guest00,guest01,guest02 — 所属するユーザ一覧 |
アクセス制限の実験
/home0/Challenge/AccesControl に、いくつかのファイルが保存してあり、t-saitoh が見ると、以下のようなファイルであった。tree コマンドでは、いくつかのディレクトリとその中のファイルが確認できる。しかし、ls -al にてファイルのアクセス権限が確認できる。tree コマンドで確認できるファイルにアクセスすると何が起こるか確認すること。
$ cd /home0/Challenge/AccessControl $ id # 自分のID,グループを確認 uid=1200(guest00) gid=1200(guests) groups=1200(guests) $ tree # ディレクトリ構造を表示 $ ls -al # 権限情報を表示 $ cd /home0/Challenge/AccessControl
$ id # 自分のID,グループを確認
uid=1200(guest00) gid=1200(guests) groups=1200(guests)
$ tree # ディレクトリ構造を表示
$ ls -al # 権限情報を表示 $ cd /home0/Challenge/AccessControl $ id # 自分のID,グループを確認 uid=1200(guest00) gid=1200(guests) groups=1200(guests) $ tree # ディレクトリ構造を表示 $ ls -al # 権限情報を表示 |
|
![]() |
![]() |
Windows とアクセスコントロール
Unix のシステムでは、ファイル毎に、ユーザID,グループIDを割り当て、ユーザ, グループ, その他に対して、Read, Write などの制限をかける。Windows では、さらに細かくアクセス制限を加えることができる。Windows では、1つのファイルに対して、ユーザやグループのRead/Writeなどの制限をいくつでも設定できる。Access Control List と呼ばれる。
主要なディレクトリとファイルシステム
unix では、すべてのデバイスを / (ルートディレクトリ) 配下に木構造につなげて管理している。CD-ROM や USB ディスクなどは、指定したディレクトリに mount (マウント) して使用する。
ext4 は、Linux で採用されているファイルシステムで、システムの保存に使われる。
tmpfs は、主記憶(D-RAM) の一部を、ディスクと同じように扱えるようにしたファイルシステム。通称 ram disk(ラムディスク)。保存はメモリへのアクセスなので、保存やアクセスは極めて高速だが、保存領域は少ない。高速に扱えて、システムが再起動された時に消えても問題のない情報を保存するために使われる。
proc は、実行中のプロセス情報を、ハードディスクに保存されたファイルの様に参照できる。
vfat , exfat は、USBメモリ, SDカード のデータ保存で使われるファイルシステムで、Windows(MS-DOS) で使われている保存形式。ファイルにファイル所有者などの概念がない。
ntfs は、Windows で使われているファイル形式。
swap は、仮想メモリのためのデータが保存される。主記憶メモリが不足した際に、使用頻度の少ないメモリ領域をハードディスクに保存するための領域。以下のような free コマンドで使用状況が確認できる。一般的に、主記憶メモリの数倍を割り当てる。
派生と継承と仮想関数
前回の派生と継承のイメージを改めて記載する。
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( const char s[] , int x , const char d[] , int g ) : Person( s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; saitoh.print() ; // 表示 t-saitoh 55 yama.print() ; // 表示 yamada 21 // - ES 1 nomu.print() ; // 表示 nomura 22 return 0 ; // - PS 2 }
このような処理でのデータ構造は、次のようなイメージで表される。
派生クラスでの問題提起
基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。
以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。
int main() { Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { table[ i ]->print() ; } return 0 ; }
C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。
このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。
t-saitoh 55 yamada 21 nomura 22
派生した型に応じた処理
上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?
// 混在したPersonを表示 for( int i = 0 ; i < 3 ; i++ ) table[i]->print() ; // Student は、所属と名前を表示して欲しい t-saitoh 55 yamada 21 - ES 1 nomura 22 - PS 2
上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という型の識別番号を追加し、type=1ならば、Student型として扱うようにしてみた。
// 基底クラス class Person { private: int type ; // 型識別情報 char name[ 20 ] ; int age ; public: Person( int tp , const char s[] , int x ) : type( tp ) , age( x ) { strcpy( name , s ) ; } int type_person() { return type ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( int tp , const char s[] , int x , const char d[] , int g ) : Person( tp , s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { // type=0 は Person 型、type=1は Student 型 Person saitoh( 0 , "t-saitoh" , 55 ) ; Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ; Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { switch( table[i]->type_person() ) { case 0 : table[i]->print() ; break ; case 1 : // 強制的にStudent*型として print() を呼び出す。 // 最近のC++なら、(static_cast<Student*>(table[i]))->print() ; ((Student*)table[i])->print() ; break ; } } return 0 ; }
しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。
また、関数を呼び出す際に、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。
もし、派生したクラスの種類がいくつもあるのなら、(1)型情報の代入は注意深く書かないとバグの元になるし、(2)型に応じた分岐処理は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミング(初期のX11)では、巨大な switch 文が問題となっていた。巨大な switch 文は、選択肢だけの if else-if else-if が並ぶと処理効率も悪い。
仮想関数
上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。
型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } virtual void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( const char s[] , int x , const char d[] , int g ) : Person( s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } virtual void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { // type=0 は Person 型、type=1は Student 型 Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { table[i]->print() ; } return 0 ; }
クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。
また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。
関数ポインタ
仮想関数の仕組みを実現するためには、関数ポインタが使われる。
以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。
int add( int a , int b ) {return a + b ;}int mul( int a , int b ) {return a * b ;}int main() {int (*fp)( int , int ) ;fp = add ;printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7fp = mul ;printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12int (*ftable[2])( int , int ) = {add , mul ,} ;for( int i = 0 ; i < 2 ; i++ )printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ;return 0 ;}int add( int a , int b ) { return a + b ; } int mul( int a , int b ) { return a * b ; } int main() { int (*fp)( int , int ) ; fp = add ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7 fp = mul ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12 int (*ftable[2])( int , int ) = { add , mul , } ; for( int i = 0 ; i < 2 ; i++ ) printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ; return 0 ; }int add( int a , int b ) { return a + b ; } int mul( int a , int b ) { return a * b ; } int main() { int (*fp)( int , int ) ; fp = add ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7 fp = mul ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12 int (*ftable[2])( int , int ) = { add , mul , } ; for( int i = 0 ; i < 2 ; i++ ) printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ; return 0 ; }仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。
仮想関数の実装方法
仮想関数の一般的な実装方法としては、仮想関数を持つオブジェクトには型情報として仮想関数へのポインタテーブルへのポインタを保存する。この場合、仮想関数の呼び出しは、object->table[n]( arg… ) のような処理が行われる。