構造体の入出力、ランダムアクセス

システム・プログラム

                                       電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/syspro-2001/2001-05-14
あるいは、次のページから手繰っていくこともできます。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/
http://www.is.tsukuba.ac.jp/~yas/index-j.html

■正誤表

System V 系で、現在の時刻を取得するには、time() シ ステムコールを使う。times() ではない。 練習問題 24を選ぶ人は、特に注意する。

exec-date.c で execvp() は、execve() の間違 いである。environ の型も違っていた。ヘッダ・ファイルも include した方 がよい。

■復習

プロセスの操作(生成、殺す)、資源割り当てと保護。 属性。ps コマンド。kill コマンド。

システムコールでのプロセスの扱い

パイプの扱い

■今日の重要な話

パイプの扱い

■捕捉

◆fork

fork() は、プロセスをコピーする。
----------------------------------------------------------------------
   1:	/*
   2:	        fork-hello.c -- 画面に文字列を表示するプログラム
   3:	        ~yas/syspro/proc/fork-hello.c
   4:	        $Header: /home/lab2/OS/yas/syspro/proc/RCS/fork-hello.c,v 1.1 2001/05/13 14:20:45 yas Exp $
   5:	        Start: 2001/05/13 23:19:01
   6:	*/
   7:	
   8:	main()
   9:	{
  10:	        fork();
  11:	        fork();
  12:	        fork();
  13:	        printf("hello\n");
  14:	}
----------------------------------------------------------------------

実行結果


----------------------------------------------------------------------
% ./fork-hello [←]
hello
hello
hello
hello
hello
hello
hello
hello
% []
----------------------------------------------------------------------

◆execve()

execve() は、プロセスはコピーしないが、プログラムを切替える。
----------------------------------------------------------------------
   1:	/*
   2:	        exec-date.c -- 実行中のプロセスのプログラムをdateプログラムに切替える
   3:	        ~yas/syspro/proc/exec-date.c
   4:	        $Header: /home/lab2/OS/yas/syspro-2001/proc/RCS/exec-date.c,v 1.2 2001/05/20 07:36:11 yas Exp $
   5:	        Start: 2001/05/13 23:25:49
   6:	*/
   7:	
   8:	#include <unistd.h>
   9:	
  10:	extern char *environ[] ;
  11:	
  12:	main()
  13:	{
  14:	    char *argv[2] ;
  15:	        argv[0] = "/bin/date" ;
  16:	        argv[1] = 0 ;
  17:	        execve( argv[0],argv,environ );
  18:	        execve( argv[0],argv,environ );
  19:	        execve( argv[0],argv,environ );
  20:	        execve( argv[0],argv,environ );
  21:	}
----------------------------------------------------------------------

実行結果


----------------------------------------------------------------------
% ./exec-date [←]
Sun May 13 23:31:51 JST 2001
% []
----------------------------------------------------------------------

一度、/bin/date が exec されたら、元のプログラムにもどることはない。 date は、仕事が終ると exit() する。

◆grep

grep (egrep, fgrep) は、ファイルの内容を検索し、引数で与えられたパタン が見つかった行を出力するコマンドである。次の例は、「UTMP_FILE」 または、「WTMP_FILE」という文字列を含む行を、ファイル /usr/include/utmp.h から探し、結果を表示している。

----------------------------------------------------------------------
% egrep '[UW]TMP_FILE' /usr/include/utmp.h [←]
#define UTMP_FILE       "/var/adm/utmp"
#define WTMP_FILE       "/var/adm/wtmp"
% []
----------------------------------------------------------------------
一般的には、次のようになる。
----------------------------------------------------------------------
% egrep pattern file1 file2 ... [←]
----------------------------------------------------------------------
pattern としては、次のような正規表現(regular expressions)が使える。
.
任意の1文字
.*
任意の1文字が0回以上繰り返したもの
A*
文字Aが0回以上繰り返したもの
A*
文字Aが0回以上繰り返したもの
^
行の先頭
$
行の末尾
[a-m]
文字aから文字mまでの1文字
[A-Za-z0-9]
アルファベットと数字
シェルで「*」と書く所は、grep では「'.*'」と書く点に注意する。 たとえば、 シェルで「*.[ch]」と書く所は、grep では「'.*\.[ch]'」と書く。

「*」、「$」、「[]」など、シェルが展開してしまう文字をパタンとして指定 する時には、シングル・クォート「''」で括る。

egrep は、機能が拡張され、アルゴリズムも改良されていて速い。

◆wait(2)

wait() システムコールの使い方に注意。Irix (POSIX) では、引数が必要である。

	wait( 0 );
または
	int status;
	wait( &status );
status には、exit() システムコールの引数で指定された値や kill() で殺さ れた時の状態が入る。

waitの引数の型は、int *だが、int 型に変数に「&」を付ける。 次のようなプログラムは、間違いである。


----------------------------------------------------------------------
main()
{
    int *statptr ;
	....
	if( fork() ) 
	....
	wait( statptr );
}
----------------------------------------------------------------------

ポインタを使う時には、その変数になんらかの方法で確保されたメモリの番地 をセットしておく必要がある。次のいずれかが必要である。

◆小さなプログラムの収集

システムコールやライブラリ関数を自分のものにするには、それを利用した、 簡単で「動く」プログラムを自分で作って整理して保存しておくとよい。

インターネット上に落ちている例を拾ってきてもよいが、短くて動く形で整理 しておかないと、自分のものにはならない。さらに、インターネット上に落ち ているものの品質を見極める目が必要である。間違いが含まれていることが多々 ある。

■時刻の扱い

Unix では、カレンダーの時刻は、1970年1月1日0:00(GMT)からの秒数で表して いる。これを time_t (または clock_t )と表している。time_t の実体は、普 通、32ビットの整数である。

現在の時刻を取得するには、time() システム・コール(System V系)や gettimeofday() システム・コール (BSD系)を使う。

その秒数からカレンダーに便利な形式(struct tm)に変換するものが、 localtime() ライブラリ関数である。struct tm は、次のようなフィールドが ある。

struct    tm {
     int  tm_sec;   /* seconds after the minute - [0, 61] */
			 /* for leap seconds */
     int  tm_min;   /* minutes after the hour - [0, 59] */
     int  tm_hour;  /* hour since midnight - [0, 23] */
     int  tm_mday;  /* day of the month - [1, 31] */
     int  tm_mon;   /* months since January - [0, 11] */
     int  tm_year;  /* years since 1900 */
     int  tm_wday;  /* days since Sunday - [0, 6] */
     int  tm_yday;  /* days since January 1 - [0, 365] */
     int  tm_isdst; /* flag for alternate daylight */
			 /* savings time */
};

ctime(), strftime() などを使うと、文字列に変換してくれる。

逆に、struct tm から time_t を作る関数が mktime() ライブラリ関数である。

localtime() や mktime() は、TZ環境変数(Time Zone)を見ている。閏秒など が混じると単純には計算できない。

■構造体(固定長)の入出力

構造体を読み書きするには、システムコール read(), write() または、高水 準(ライブラリ)の fread(), fwrite() を使う。

struct struct1 var1 ;
FILE *fp ;
...
fread(&var1, sizeof(struct1),1,fp );

または
struct struct1 var1 ;
int fd ;

read(fd, &var1, sizeof(struct1) );

構造体をファイルに保存する時に注意することは、ポインタが(直接的には) 使えないことである。

さらにポータビリティ(別のOSやCPUのコンピュータでも使う)を考える 時には、 文字コードバイトオーダー、 浮動小数点の形式、構造体の穴に注意する。

Cコンパイラは、構造体に見えない要素(穴)を追加して、int や double を、 4バイト境界に合うようにしてしまうことがある。 (XDR ライブラリを使えば、自動的にこのあたりのことを変換してくれる。 XDR については、3学期の「分散システム」で述べる。)

----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        utmp-print.c -- /var/adm/utmp の内容を表示するプログラム
   4:	        ~yas/syspro/file/utmp-print.c
   5:	        $Header: /home/lab2/OS/yas/syspro/file/RCS/utmp-print.c,v 1.5 2001/05/13 11:08:59 yas Exp $
   6:	        Start: 1995/03/06 09:01:09
   7:	*/
   8:	
   9:	#include <stdio.h>      /* FILE, NULL, stderr */
  10:	#include <utmp.h>       /* struct utmp */
  11:	#include <time.h>       /* ctime() */
  12:	
  13:	#if     0
  14:	struct utmp
  15:	{
  16:	    char ut_user[8] ;           /* User login name */
  17:	    char ut_id[4] ;             /* /etc/inittab id(usually line no) */
  18:	    char ut_line[12] ;          /* device name (console, lnxx) */
  19:	    short ut_pid ;              /* leave short for compatiblity - process id */
  20:	    short ut_type ;             /* type of entry */
  21:	    struct exit_status ut_exit ;/* The exit status of a process
  22:	                                 * marked as DEAD_PROCESS.
  23:	                                 */
  24:	    time_t ut_time ;            /* time entry was made */
  25:	} ;
  26:	#endif
  27:	
  28:	void utmp_print_file();
  29:	void utmp_print( struct utmp *u );
  30:	void print_char_array( char a[], int len );
  31:	
  32:	main()
  33:	{
  34:	        utmp_print_file();
  35:	}
  36:	
  37:	void utmp_print_file()
  38:	{
  39:	    struct utmp entry;
  40:	    FILE        *fp ;
  41:	        fp = fopen(  UTMP_FILE, "r" );
  42:	        if( fp == NULL )
  43:	        {
  44:	            perror( UTMP_FILE );
  45:	            exit( -1 );
  46:	        }
  47:	        while( fread(&entry, sizeof(entry),1,fp ) == 1 )
  48:	        {
  49:	            utmp_print( &entry );
  50:	        }
  51:	        fclose( fp );
  52:	}
  53:	
  54:	void utmp_print( struct utmp *u )
  55:	{
  56:	        print_char_array( u->ut_user, sizeof(u->ut_user) );
  57:	        print_char_array( u->ut_id, sizeof(u->ut_id) );
  58:	        print_char_array( u->ut_line, sizeof(u->ut_line) );
  59:	        printf("%6d ", u->ut_pid );
  60:	        printf("%6d ", u->ut_type );
  61:	        printf("%s",ctime(&u->ut_time) );
  62:	}
  63:	
  64:	void print_char_array( char a[], int len )
  65:	{
  66:	    int i ;
  67:	        for( i=0 ; i<len && a[i] ; i++ )
  68:	            putchar( a[i] );
  69:	        for( ; i<len ; i++ )
  70:	            putchar(' ');
  71:	        putchar(' ');
  72:	}
  73:	
  74:	/* ------------------------ utmp-print.c ------------------------ */
  75:	static char rcsid[] =
  76:	"$Header: /home/lab2/OS/yas/syspro/file/RCS/utmp-print.c,v 1.5 2001/05/13 11:08:59 yas Exp $" ;
----------------------------------------------------------------------

ut_time には、1970年1月1日0:00(GMT)からの秒数で時刻 が含まれている。

実行結果


----------------------------------------------------------------------
% ./utmp-print [←]
              system boot       0      2 Fri May 11 10:54:20 2001
              run-level 2       0      1 Fri May 11 10:54:20 2001
lnsyscon link                  34      8 Fri May 11 10:54:20 2001
rc2      s2                    37      8 Fri May 11 10:55:44 2001
LOGIN    t1   ttyd1           702      6 Fri May 11 10:55:44 2001
ken-ichi q0   ttyq0          7165      7 Sun May 13 20:28:56 2001
i961351  q1   ttyq1          3416      7 Fri May 11 20:09:51 2001
yas      q2   ttyq2          7050      7 Sun May 13 20:04:46 2001
yas      q3   ttyq3          7082      7 Sun May 13 20:05:37 2001
yamajyun q4   ttyq4          7196      8 Sun May 13 20:50:41 2001
% who [←]
ken-ichi   ttyq0         5月 13日 20時28分
i961351    ttyq1         5月 11日 20時09分
yas        ttyq2         5月 13日 20時04分
yas        ttyq3         5月 13日 20時05分
% []
----------------------------------------------------------------------

ctime() は、文字列のの最後に改行 \n を含めてしまう。改行を含まないよう な文字列が必要な時には、localtime() を使うか、strftime() を使う。

■lseek()システム・コールによるファイルのランダムアクセス

UNIXでは、ファイルを read() したり write() したりするたびに、ファイル を読み書きする位置(先頭からのバイト数)が移動する。この位置は、シーク・ ポインタやファイル・ポインタと呼ばれる。(FILE *とは違うので注意。)

ファイルを先頭から順にアクセスするのではなく、ランダムにアクセスしたい 時には、まず読み書きしたい場所にシーク・ポインタを移動させる。これを行 うのが、lseek() システム・コールである。

off_t lseek(int fildes, off_t offset, int whence);
引数は、ファイル記述子、オフセット(バイト数)、移動方法である。移動方法 (whence)には、次の3種類のいずれかを指定する。
SEEK_SET
ファイルの先頭から offset バイト目に移動。
SEEK_CUR
現在の位置から offset バイト目に移動。
SEEK_END
ファイルの末尾から offset バイト目に移動。
ファイルの末尾から読むプログラムの例。

----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        wtmp-last10.c -- /var/adm/wtmpの最後の10個を表示するプログラム
   4:	        ~yas/syspro/file/wtmp-last10.c
   5:	        $Header: /home/lab2/OS/yas/syspro/file/RCS/wtmp-last10.c,v 1.4 2001/05/13 13:22:24 yas Exp $
   6:	        Start: 1995/03/06 09:01:09
   7:	*/
   8:	
   9:	#include <stdio.h>      /* FILE, NULL, stderr */
  10:	#include <utmp.h>       /* struct utmp */
  11:	#include <sys/types.h>  /* open(),lseek() */
  12:	#include <sys/stat.h>   /* open() */
  13:	#include <fcntl.h>      /* open() */
  14:	#include <unistd.h>     /* lseek() */
  15:	
  16:	void wtmp_last10_print_file();
  17:	void utmp_print( struct utmp *u );
  18:	void print_char_array( char a[], int len );
  19:	
  20:	main()
  21:	{
  22:	        wtmp_last10_print_file();
  23:	}
  24:	
  25:	void wtmp_last10_print_file()
  26:	{
  27:	    struct utmp  entry;
  28:	    int fd, i ;
  29:	    off_t offset ;
  30:	
  31:	        if( (fd = open(  WTMP_FILE, O_RDONLY )) == -1 )
  32:	        {
  33:	            perror( WTMP_FILE );
  34:	            exit( -1 );
  35:	        }
  36:	        for( i=-1 ; i>-10; i-- )
  37:	        {
  38:	            offset = sizeof(struct utmp)*i ;
  39:	
  40:	            if( lseek( fd, offset, SEEK_END ) == -1 )
  41:	            {
  42:	                perror("lseek");
  43:	                exit( -1 );
  44:	            }
  45:	            if( read(fd, &entry, sizeof(entry)) != sizeof(entry) )
  46:	            {
  47:	                perror("read");
  48:	                exit( -1 );
  49:	            }
  50:	            utmp_print( &entry );
  51:	        }
  52:	        close( fd );
  53:	}
  54:	
  55:	void utmp_print( struct utmp *u )
<以下略>
----------------------------------------------------------------------

実行例。
----------------------------------------------------------------------
% ./wtmp-last10 [←]
yas      q4   ttyq4         16360      7 Tue May 12 00:07:23 1998
rlogin   q4   ttyq4         16360      6 Tue May 12 00:07:23 1998
telnet   q3   ttyq3         16201      8 Mon May 11 23:47:57 1998
e96????  q3   ttyq3         16202      7 Mon May 11 23:43:51 1998
telnet   q3   ttyq3         16202      6 Mon May 11 23:43:47 1998
a???     q1   ttyq1         16110      7 Mon May 11 23:33:52 1998
telnet   q1   ttyq1         16110      6 Mon May 11 23:33:49 1998
telnet   q1   ttyq1         16063      8 Mon May 11 23:33:43 1998
a???     q1   ttyq1         16064      7 Mon May 11 23:08:18 1998
% last | head -10 [←]
yas      ttyq4        hlla-gw.hlla.is.tsuku Tue May 12 00:07   still logged in
e96????  ttyq3        130.158.125.185       Mon May 11 23:43 - 23:47  (00:04)
a???     ttyq1        AMIcc-01p08.ppp.xxxxx Mon May 11 23:33   still logged in
a???     ttyq1        AMIcc-01p08.ppp.xxxxx Mon May 11 23:08 - 23:33  (00:25)
k?????   ttyq0        balsam-serv           Mon May 11 22:31   still logged in
t????    ttyq1        adonis7               Mon May 11 21:49 - 21:49  (00:00)
to????   ttyq0        icho.ipe.tsukuba.ac.j Mon May 11 21:38 - 22:01  (00:23)
m????    ttyq0        azalea11              Mon May 11 21:06 - 21:17  (00:11)
to????   ttyq4        icho.ipe.tsukuba.ac.j Mon May 11 19:06 - 19:09  (00:03)
m????    ttyq4        azalea11              Mon May 11 18:01 - 18:07  (00:05)
% []
----------------------------------------------------------------------

◆fseek

高水準入出力ライブラリ を使う時には、lseek() ではなく fseek() を使う。

◆mmap

mmap() システム・コールを使うと、ファイルをメモリに張り付けることがで きる。これを使うと、ファイルの内容を全てある番地からメモリに読み込んだ ように扱うことができる。実際には、アクセスした部分だけしかメモリに読み 込まれない。

■mmap

ファイルの内容を操作する方法として、read(), write() とは全く違った方法 がある。それは、ファイルの内容をメモリにマップ(map, 張り付ける)し、 ファイルの内容を配列のようにして扱う方法である。


----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        mmap-head.c -- mmap(2) を使った head プログラム
   4:	        $Header: /home/lab2/OS/yas/syspro/file/RCS/mmap-head.c,v 1.2 2001/05/13 13:46:35 yas Exp $
   5:	        ~yas/syspro/file/mmap-head.c
   6:	        Start: 1998/05/12 01:06:15
   7:	*/
   8:	
   9:	#include <stdio.h>      /* stderr */
  10:	#include <sys/types.h>  /* open(),mmap() */
  11:	#include <sys/stat.h>   /* open() */
  12:	#include <fcntl.h>      /* open() */
  13:	#include <sys/mman.h>   /* mmap() */
  14:	
  15:	extern void mmap_head( char *name );
  16:	extern char *mmap_file( char *name /* in */, size_t *filesizep /* out */);
  17:	
  18:	void main( int argc, char *argv[] )
  19:	{
  20:	        if( argc != 2 )
  21:	        {
  22:	            fprintf( stderr,"Usage: %% %s filename\n",argv[0] );
  23:	            exit( 1 );
  24:	        }
  25:	        mmap_head( argv[1] );
  26:	}
  27:	
  28:	void mmap_head( char *name )
  29:	{
  30:	    size_t size,i ;     /* unsigned int */
  31:	    char *file_address ;
  32:	    int nlcount ;
  33:	
  34:	        file_address = mmap_file( name,&size );
  35:	        if( ((int)file_address) == -1 )
  36:	        {
  37:	            perror( name );
  38:	            exit( 1 );
  39:	        }
  40:	        for( i = 0, nlcount=0 ; i<size ; i ++ )
  41:	        {
  42:	            if( file_address[i] == '\n' )
  43:	            {
  44:	                 nlcount ++ ;
  45:	                 if( nlcount >= 10 )
  46:	                 {
  47:	                     i++ ;
  48:	                     break;
  49:	                 }
  50:	            }
  51:	        }
  52:	        write( 1, file_address, i );
  53:	}
  54:	
  55:	char *mmap_file( char *name /* in */, size_t *filesizep /* out */)
  56:	{
  57:	    int fd ;
  58:	    struct stat buf ;
  59:	    size_t size,p ;     /* unsigned int */
  60:	    off_t off ;         /* long */
  61:	    caddr_t addr ;      /* char * */
  62:	        if( (fd = open( name,O_RDONLY )) == -1 )
  63:	            return( (char *)-1 );
  64:	        fstat( fd, &buf );
  65:	        size = buf.st_size ;
  66:	        addr = 0 ;      off = 0 ;
  67:	        addr = mmap( addr, size, PROT_READ, MAP_PRIVATE, fd, off );
  68:	        if( (int)addr == -1 )
  69:	        {
  70:	            close( fd );
  71:	            return( (char *)-1 );
  72:	        }
  73:	        close( fd );
  74:	        if( filesizep )
  75:	            *filesizep = size ;
  76:	        return( addr );
  77:	}
----------------------------------------------------------------------

実行例。
----------------------------------------------------------------------
% ./mmap-head /etc/passwd  [←]
root:sBcDxfGhijklmx:0:0:Super-User:/:/bin/csh
sysadm:*:0:0:System V Administration:/usr/admin:/bin/sh
diag:*:0:996:Hardware Diagnostics:/usr/diags:/bin/csh
daemon:*:1:1:daemons:/:/dev/null
bin:*:2:2:System Tools Owner:/bin:/dev/null
uucp:*:3:5:UUCP Owner:/usr/lib/uucp:/bin/csh
sys:*:4:0:System Activity Owner:/var/adm:/bin/sh
adm:*:5:3:Accounting Files Owner:/var/adm:/bin/sh
lp:*:9:9:Print Spooler Owner:/var/spool/lp:/bin/sh
nuucp:*:10:10:Remote UUCP User:/var/spool/uucppublic:/usr/lib/uucp/uucico
% []
----------------------------------------------------------------------

■練習問題

★練習問題 23 dateコマンド

date コマンドと似たプログラム(現在の時刻を表示するプログラム)を、 time(), gettimeofday(), ctime() などを使って作りなさい。

★練習問題 24 localtime(3)、strftime(3) の利用

localtime(3)やstrftime(3)を使って、ctime() や dateコマンドとは異なる形 式で時刻の表示するプログラムを作りなさい。

★練習問題 25 lastコマンド

wtmp ファイルを読み込み、last コマンドと似た動きをするプログラムを作り なさい。

この課題では、wtmp にある情報だけが表示できればよい。 wtmp ではなく wtmpx を使うと、ホスト名まで調べられる。

★練習問題 26 出席判定プログラム

あるユーザが、ある与えられた時刻の範囲内にログインしていたかどうかを調 べるプログラムを作りなさい。引数の与え方、結果の表示の方法は、各自で設 計する。キーボードから対話的に使うのではなく、main() の引数やファイル からデータを読み込むようにするほうが好ましい。

たとえば、引数として日付、時刻、ユーザ名 user1, user2 ... のユーザが、 その時間にログインしていたら、yes を返す。


% present 2001/05/14 11:30 2001/05/14 13:00 user1 user2 ... [←]
user1	yes
user2	no
...
% []
ヒント:まず struct tm に日付と時間をセットして、 mktime() を使って time_t を得る。ログイン時間とログアウトの時間を wtmp ファイルで調べる。 指定されていた範囲にかかれば、yes と判定する。

★練習問題 27 tail プログラム

lseek() システム・コール、 mmap() システム・コール、 または、 fseek() ライブラリ関数 を使って、tail コマンドに似たコマンドを作り なさい。

tail コマンドは、head コマンドとは反対に、ファイルの末尾を表示する ものである。