パイプ、構造体の入出力、ランダムアクセス、時刻の扱い

システム・プログラム

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

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

■復習

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

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

■今日の重要な話

パイプの扱い。 時刻の扱い。 構造体の入出力とランダムアクセス

■sizeof

sizeof(X)と書くと、X の大きさをバイト数で得られる。これは、関数ではな く、Cコンパイラが、コンパイル時に定数に置き換える。X には、型(int, char, struct xxx)の他に、変数も書ける。sizeof() は、strlen() の変りに は使えない。

C言語では、(C++とは異なり)構造体の名前だけでは、型にならない。頭 にstruct を付けると型になる。sizeof() に与える時には、struct も含める。

sizeof() に変数を指定した場合、ポインタは常に4バイトになる。ポインタ の先のバイト数ではない。配列は、配列全体の大きさにる。


----------------------------------------------------------------------
   1:	/*
   2:	        sizeof.c -- sizeof() の意味
   3:	        ~yas/syspro/cc/sizeof.c
   4:	        Start: 2003/05/11 16:45:52
   5:	*/
   6:	
   7:	#include <stdio.h> /* stderr */
   8:	#include <stdlib.h> /* malloc() */
   9:	
  10:	
  11:	char a1[] = { 'h','e','l','l','o',0 };
  12:	char *s1 = "hello";
  13:	
  14:	main()
  15:	{
  16:	    char *p ;
  17:	        printf("sizeof(char) == %d\n",sizeof(char));
  18:	        printf("sizeof(int) == %d\n",sizeof(int));
  19:	        printf("sizeof(char *) == %d\n",sizeof(char *));
  20:	
  21:	        printf("sizeof(p) == %d\n",sizeof(p));
  22:	        p = malloc( 100 );
  23:	        if( p == 0 )
  24:	        {
  25:	            fprintf(stderr,"no memory\n");
  26:	            exit( 1 );
  27:	        }
  28:	        printf("after malloc, sizeof(p) == %d\n",sizeof(p));
  29:	        free( p );
  30:	        printf("after free, sizeof(p) == %d\n",sizeof(p));
  31:	
  32:	        printf("%%s a1: %s, strlen(a1)==%d\n",a1,strlen(a1));
  33:	        printf("%%s s1: %s, strlen(s1)==%d\n",s1,strlen(s1));
  34:	        printf("sizeof(a1) == %d\n",sizeof(a1));
  35:	        printf("sizeof(s1) == %d\n",sizeof(s1));
  36:	        printf("sizeof(\"hello\") == %d\n",sizeof("hello"));
  37:	}
----------------------------------------------------------------------

実行例。

----------------------------------------------------------------------
% cp ~yas/syspro/cc/sizeof.c . [←]
% make sizeof [←]
cc     sizeof.c   -o sizeof
% ./sizeof  [←]
sizeof(char) == 1
sizeof(int) == 4
sizeof(char *) == 4
sizeof(p) == 4
after malloc, sizeof(p) == 4
after free, sizeof(p) == 4
%s a1: hello, strlen(a1)==5
%s s1: hello, strlen(s1)==5
sizeof(a1) == 6
sizeof(s1) == 4
sizeof("hello") == 6
% []
----------------------------------------------------------------------

配列とポインタは、sizeof() で見ると違う。ポインタは、どんなポインタで も固定長になる。現在の coins の環境では、4バイトである。

■pipe() と dup()

シェルで、次のようなプログラムを動かすこと考える。

----------------------------------------------------------------------
% echo "hello,world" | tr '[a-z]' '[A-Z]' [←]
HELLO,WORLD
% []
----------------------------------------------------------------------

この例では、echo のプロセスと、tr のプロセスは、パイプで接続されている。 パイプは、open() したファイルと同じようにread() したり write() したり することがでる。しかし実際には、ファイルではなく、プロセスとプロセスが データを交換する仕組(プロセス間通信の仕組)の1つである。片方のプロセ スが write() したデータを、もう片方のプロセスがread() できる。

図4−0 パイプによるプロセス間通信

図4−0 パイプによるプロセス間通信

パイプにより提供されるプロセス間通信のサービスは、 (単方向の)ストリームとも呼ばれる。FIFO (First In, First Out)や Queue (待ち行列) と呼ばれることもある。

図4−0 バッファによるパイプの実現

図4−0 バッファによるパイプの実現

内部的には、8k バイト程度のバッファ(メモリ)がオペレーティング・システ ムのカーネルにある。バッファが一杯になると、書込み側のプロセスは write() で止まる(write() システムコールの呼び出しが元にもどらない。) バッファが空だと、read() で止まる。 全部の書込み側の口が close() されると、read() で EOF が返る。

パイプを作るには、pipe() システム・コールを使う。これで、パイプ が1本作られ、2つのファイル記述子(読込み用と書込み用)が返される。


----------------------------------------------------------------------
   1:	/*
   2:	        pipe-rw-nodup.c -- pipe() を使ったプログラム(dupなし)
   3:	        ~yas/syspro/proc/pipe-rw-nodup.c
   4:	        Start: 1997/05/26 20:43:29
   5:	*/
   6:	
   7:	#include <stdio.h>
   8:	#include <unistd.h>     /* pipe() */
   9:	#include <sys/types.h>  /* pid_t */
  10:	
  11:	extern  void parent( int fildes[2] );
  12:	extern  void child( int fildes[2] );
  13:	
  14:	main()
  15:	{
  16:	    int fildes[2] ;
  17:	    pid_t pid ;
  18:	
  19:	        if( pipe(fildes) == -1)
  20:	        {
  21:	            perror("pipe");
  22:	            exit( 1 );
  23:	        }
  24:	        /* fildes[0] -- 読み込み用
  25:	         * fildes[1] -- 書き込み用
  26:	         */
  27:	        if( (pid=fork()) == 0 )
  28:	        {
  29:	            child( fildes );
  30:	            exit( 0 );
  31:	        }
  32:	        else if( pid > 0 )
  33:	        {
  34:	            parent( fildes );
  35:	            wait( 0 );
  36:	        }
  37:	        else
  38:	        {
  39:	            perror("fork");
  40:	            exit( 1 );
  41:	        }
  42:	}
  43:	
  44:	void parent( int fildes[2] )
  45:	{
  46:	    char *p, c ;
  47:	        close( fildes[0] );
  48:	        p = "hello,world\n" ;
  49:	        while( *p )
  50:	        {
  51:	            c = *p ++ ;
  52:	            write( fildes[1],&c,1 );
  53:	        }
  54:	        close( fildes[1] );
  55:	}
  56:	
  57:	void child( int fildes[2] )
  58:	{
  59:	    char c, c2 ;
  60:	        close( fildes[1] );
  61:	        while( read(fildes[0],&c,1) >0 )
  62:	        {
  63:	            c2 = toupper(c);
  64:	            write( 1, &c2, 1 );
  65:	        }
  66:	        close( fildes[0] );
  67:	}
----------------------------------------------------------------------

実行例。

----------------------------------------------------------------------
% cp ~yas/syspro/proc/pipe-rw-nodup.c . [←]
% make pipe-rw-nodup [←]
cc     pipe-rw-nodup.c   -o pipe-rw-nodup
% ./pipe-rw-nodup [←]
HELLO,WORLD
% []
----------------------------------------------------------------------

親プロセス(関数parent()を実行)は、パイプの読込み側(fildes[0])を閉じる。 そして、文字列から1バイトずつ取り出し、0 が現れるまで、パイプの書込み 側(fildes[1])に書込む。

子プロセス(関数child()=を実行)は、パイプの書込み側(fildes[1])を閉じる。 そしてパイプの読込み側(fildes[0])から1バイト取り出す。これをtoupper() で大文字に変換して、標準出力(1)に書き出す。

pipe() システムコール実行前

図4−1 pipe() システムコール実行前

pipe() システムコール実行後

図4−2 pipe() システムコール実行後

fork() システムコール実行後

図4−3 fork() システムコール実行後

親子で close() システムコール実行後

図4−4 親子で close() システムコール実行後

◆dup()

pipe() システム・コールで作られたファイル記述子は、しばしば dup() シス テム・コールで、0, 1 に変えられる。dup() システム・コールは、記述子を コピーするものである。小さい数字から探していくので、たとえば close(0) の直後に dup(fd) すると、fd が 0 にコピーされる。

dup() よりも、dup2() の方が便利である。


----------------------------------------------------------------------
   1:	/*
   2:	        pipe-rw.c -- pipe() と dup() を使ったプログラム
   3:	        ~yas/syspro/proc/pipe-rw-dup.c
   4:	        Start: 1997/05/26 20:43:29
   5:	*/
   6:	
   7:	#include <stdio.h>
   8:	#include <unistd.h>     /* pipe() */
   9:	#include <sys/types.h>  /* pid_t */
  10:	
  11:	extern  void parent( int fildes[2] );
  12:	extern  void child( int fildes[2] );
  13:	
  14:	main()
  15:	{
  16:	    int fildes[2] ;
  17:	    pid_t pid ;
  18:	
  19:	        if( pipe(fildes) == -1)
  20:	        {
  21:	            perror("pipe");
  22:	            exit( 1 );
  23:	        }
  24:	        /* fildes[0] -- 読み込み用
  25:	         * fildes[1] -- 書き込み用
  26:	         */
  27:	        if( (pid=fork()) == 0 )
  28:	        {
  29:	            child( fildes );
  30:	            exit( 0 );
  31:	        }
  32:	        else if( pid > 0 )
  33:	        {
  34:	            parent( fildes );
  35:	            wait( 0 );
  36:	        }
  37:	        else
  38:	        {
  39:	            perror("fork");
  40:	            exit( 1 );
  41:	        }
  42:	}
  43:	
  44:	void parent( int fildes[2] )
  45:	{
  46:	    char *p, c ;
  47:	        close( fildes[0] );
  48:	        close( 1 );         /* 標準出力をパイプに切替える */
  49:	        dup( fildes[1] );
  50:	        close( fildes[1] );
  51:	
  52:	        p = "hello,world\n" ;
  53:	        while( *p )
  54:	        {
  55:	            c = *p ++ ;
  56:	            write( 1,&c,1 );
  57:	        }
  58:	        close( 1 );
  59:	}
  60:	
  61:	void child( int fildes[2] )
  62:	{
  63:	    char c, c2 ;
  64:	        close( fildes[1] );
  65:	        close( 0 );         /* 標準入力をパイプに切替える */
  66:	        dup( fildes[0] );
  67:	        close( fildes[0] );
  68:	
  69:	        while( read(0,&c,1) >0 )
  70:	        {
  71:	            c2 = toupper(c);
  72:	            write( 1, &c2, 1 );
  73:	        }
  74:	        close( 0 );
  75:	}
----------------------------------------------------------------------

実行例。

----------------------------------------------------------------------
% cp ~yas/syspro/proc/pipe-rw-dup.c . [←]
% make pipe-rw-dup [←]
cc     pipe-rw-dup.c   -o pipe-rw-dup
% ./pipe-rw-dup  [←]
HELLO,WORLD
% []
----------------------------------------------------------------------

親子で close();dup();close() 後

図4−5 親子で close();dup();close() 後

親プロセス(関数parent()を実行)は、まず、パイプの読込み側(fildes[0])を 閉じている。標準出力(ファイル記述子 1) を閉じ、dup() で、fildes[1]をコ ピーしている。この時点で、write(fildes[1],...) としてもwrite(1,...,) としても同じ意味になっている。ここで不要な fildes[1] を閉じている。そ して、文字列から1バイトずつ取り出し、0 が現れるまで、標準出力(ファイ ル記述子 1)に書込む。

子プロセス(関数child()=を実行)は、まず、パイプの書込み側(fildes[1])を 閉じている。 標準入力(ファイル記述子 0) を閉じ、dup() で、fildes[0]をコ ピーしている。この時点で、read(fildes[0],...) としてもread(0,...,) としても同じ意味になっている。ここで不要な fildes[0] を閉じている。 そして標準入力(ファイル記述子 0)から1バイト取り出す。これをtoupper() で大文字に変換して、標準出力(ファイル記述子 1)に書き出す。

■時刻の扱い

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


----------------------------------------------------------------------
% egrep time_t /usr/include/bits/types.h  [←]
typedef long int __time_t;
% []
----------------------------------------------------------------------

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

#include <time.h>
time_t time(time_t *t);

#include <sys/time.h>
#include <unistd.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);

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

CTIME(3)            Linux Programmer's Manual            CTIME(3)

#include <time.h>
char *asctime(const struct tm *timeptr);
char *ctime(const time_t *timep);
struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);
time_t mktime(struct tm *timeptr);
...
struct tm
{
	int     tm_sec;                 /* 秒 */
	int     tm_min;                 /* 分 */
	int     tm_hour;                /* 時間 */
	int     tm_mday;                /* 日 */
	int     tm_mon;                 /* 月 */
	int     tm_year;                /* 年 */
	int     tm_wday;                /* 曜日 */
	int     tm_yday;                /* 年内通算日 */
	int     tm_isdst;               /* 夏時間 */
};

ctime(), strftime() などを使うと、文字列に変換してくれる。逆に、struct tm から time_t を作る関数が mktime() ライブラリ関数である。

localtime() や mktime() は、TZ環境変数(Time Zone)を見ている。閏秒など が混じると単純には計算できない。TZ がセットされていない時、Linux の場 合、/etc/localtime というファイルにあるものが使われる。


----------------------------------------------------------------------
% setenv LANG C [←]
% ls -l /etc/localtime [←]
-rw-r--r--    1 root     root           73 Feb  7  2002 /etc/localtime
% date [←]
Mon May 12 00:05:41 JST 2003
% setenv TZ EST [←]
% date [←]
Sun May 11 10:05:50 EST 2003
% ls -l /usr/share/zoneinfo/EST [←]
-rw-r--r--    6 root     root          286 Mar  6 09:07 /usr/share/zoneinfo/EST
% []
----------------------------------------------------------------------
プログラムの中で Time Zone を変えることはあまりないが、変えるなら setenv() や putenv() で TZ 環境変数を変えて、tzset() ライブラリ関数を 呼ぶ。

◆get-time


----------------------------------------------------------------------
   1:	/*
   2:	        get-time.c -- 現在の時刻を表示するプログラム
   3:	        ~yas/syspro/time/get-time.c
   4:	        Start: 1998/05/18 22:29:17
   5:	*/
   6:	
   7:	#include <time.h>       /* time(2) */
   8:	
   9:	/*
  10:	Linux:
  11:	/usr/include/bits/types.h
  12:	typedef long int __time_t;
  13:	
  14:	/usr/include/time.h:
  15:	typedef __time_t time_t;
  16:	extern time_t time (time_t *__timer) ;
  17:	*/
  18:	
  19:	main()
  20:	{
  21:	    time_t t ;
  22:	        t = 0 ;
  23:	        printf("%d: %s",t,ctime(&t) );
  24:	        t ++ ;
  25:	        printf("%d: %s",t,ctime(&t) );
  26:	        t = time( 0 );
  27:	        printf("%d: %s",t,ctime(&t) );
  28:	        t ++ ;
  29:	        printf("%d: %s",t,ctime(&t) );
  30:	}
----------------------------------------------------------------------

実行例。

----------------------------------------------------------------------
% cp ~yas/syspro/time/get-time.c . [←]
% make get-time [←]
cc     get-time.c   -o get-time
% ./get-time  [←]
0: Wed Dec 31 19:00:00 1969
1: Wed Dec 31 19:00:01 1969
1052670767: Sun May 11 11:32:47 2003
1052670768: Sun May 11 11:32:48 2003
% []
----------------------------------------------------------------------

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

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

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

または
struct struct1 var1 ;
int fd ;

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

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

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

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

◆utmp-print.c

/var/run/utmp というファイルには、そのコンピュータを現在、利用している 人の情報が書かれている。who コマンドは、この構造体を見やすい形で表示す る。
----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        utmp-print.c -- /var/run/utmp の内容を表示するプログラム
   4:	        ~yas/syspro/file/utmp-print.c
   5:	        Start: 1995/03/06 09:01:09
   6:	*/
   7:	
   8:	#include <stdio.h>      /* FILE, NULL, stderr */
   9:	#include <utmp.h>       /* struct utmp, UTMP_FILE */
  10:	#include <time.h>       /* ctime() */
  11:	
  12:	#if     0
  13:	/* Linux の場合 /usr/include/bits/utmp.h にある。 */
  14:	#define UT_LINESIZE     32
  15:	#define UT_NAMESIZE     32
  16:	#define UT_HOSTSIZE     256
  17:	
  18:	/* The structure describing an entry in the user accounting database.  */
  19:	struct utmp
  20:	{
  21:	  short int ut_type;            /* Type of login.  */
  22:	  pid_t ut_pid;                 /* Process ID of login process.  */
  23:	  char ut_line[UT_LINESIZE];    /* Devicename.  */
  24:	  char ut_id[4];                /* Inittab ID.  */
  25:	  char ut_user[UT_NAMESIZE];    /* Username.  */
  26:	  char ut_host[UT_HOSTSIZE];    /* Hostname for remote login.  */
  27:	  struct exit_status ut_exit;   /* Exit status of a process marked
  28:	                                   as DEAD_PROCESS.  */
  29:	  long int ut_session;          /* Session ID, used for windowing.  */
  30:	  struct timeval ut_tv;         /* Time entry was made.  */
  31:	  int32_t ut_addr_v6[4];        /* Internet address of remote host.  */
  32:	  char __unused[20];            /* Reserved for future use.  */
  33:	};
  34:	#endif
  35:	
  36:	void utmp_print_file();
  37:	void utmp_print( struct utmp *u );
  38:	void print_char_array( char a[], int len );
  39:	
  40:	main()
  41:	{
  42:	        utmp_print_file();
  43:	}
  44:	
  45:	void utmp_print_file()
  46:	{
  47:	    struct utmp entry;
  48:	    FILE        *fp ;
  49:	        fp = fopen(  UTMP_FILE, "r" );
  50:	        if( fp == NULL )
  51:	        {
  52:	            perror( UTMP_FILE );
  53:	            exit( -1 );
  54:	        }
  55:	        while( fread(&entry, sizeof(entry),1,fp ) == 1 )
  56:	        {
  57:	            utmp_print( &entry );
  58:	        }
  59:	        fclose( fp );
  60:	}
  61:	
  62:	void utmp_print( struct utmp *u )
  63:	{
  64:	        print_char_array( u->ut_user, sizeof(u->ut_user) );     putchar('|');
  65:	        print_char_array( u->ut_id, sizeof(u->ut_id) );         putchar('|');
  66:	        print_char_array( u->ut_line, sizeof(u->ut_line) );     putchar('|');
  67:	        printf("%010d", u->ut_pid );                            putchar('|');
  68:	        printf("%06d", u->ut_type );                            putchar('|');
  69:	        printf("%s",ctime(&u->ut_time) ); /* '\n' will be added by ctime()*/
  70:	}
  71:	
  72:	void print_char_array( char a[], int len )
  73:	{
  74:	    int i ;
  75:	        for( i=0 ; i<len && a[i] ; i++ )
  76:	            putchar( a[i] );
  77:	        for( ; i<len ; i++ )
  78:	            putchar('.');
  79:	}
----------------------------------------------------------------------

実行結果


----------------------------------------------------------------------
% cp ~yas/syspro/file/utmp-print.c . [←]
% make utmp-print [←]
cc     utmp-print.c   -o utmp-print
% ./utmp-print [←]
................................|si..|................................|000000001
0|000008|Tue May  7 17:45:29 2002
reboot..........................|~~..|~...............................|000000000
0|000002|Tue May  7 17:45:29 2002
<略>
yas.............................|/1..|pts/1...........................|000001156
6|000007|Sun May 19 23:21:56 2002
yas.............................|ts/0|pts/0...........................|000001153
4|000007|Sun May 19 23:21:49 2002
yas.............................|/3..|pts/3...........................|000001163
2|000008|Sun May 19 23:26:39 2002
<略>
% who [←]
yas      pts/1    May 19 23:21
yas      pts/0    May 19 23:21
% who -l [←]
yas      pts/1    May 19 23:21
yas      pts/0    May 19 23:21 (bridge.hlla.is.tsukuba.ac.jp)
% []
----------------------------------------------------------------------

struct utmp というデータ構造に会わせて、表示する関数が分かれている点に 注意する。データ構造が、アルゴリズムを決める。構造体なら、関数、配列な ら、ループになる。

print_char_array() では、char の配列を表示している。この構造体 utmp 場 合、C言語の文字列と違って、0 で終端されていないことがある。従って、最 大のバイト数を渡している。

ut_time には、1970年1月1日0:00(UTC)からの秒数で時刻 が含まれている。 構造体 struct utmp には、ut_timeが定義されていない。これは、他のシステ ム(BSD や Solaris)では、そういう名前のフィールドが存在する。Linux の場 合、ut_time は、struct timeval ut_tv の一部になっている。ut_time と書 いても動作するのは、次のようなマクロが含まれているからである。

/usr/include/bits/utmp.h:
#define ut_time        ut_tv.tv_sec

ut_time と書くと、BSD でも Linux でも両方で動作する。ut_tv を使うと、 Linux でしか動作しない。

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 バイト目に移動。

◆wtmp-last10.c

ファイルの末尾から読むプログラムの例。/var/log/wtmp は、過去にログイン してきた情報を保存するファイルである。

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

実行例。
----------------------------------------------------------------------
% cp ~yas/syspro/file/wtmp-last10.c . [←]
% make wtmp-last10 [←]
cc     wtmp-last10.c   -o wtmp-last10
% ./wtmp-last10  [←]
................................|....|pts/2...........................|000001161
2|000008|Sun May 19 23:26:39 2002
yas.............................|/3..|pts/3...........................|000001163
2|000008|Sun May 19 23:26:39 2002
yas.............................|/3..|pts/3...........................|000001163
2|000007|Sun May 19 23:26:37 2002
yas.............................|ts/2|pts/2...........................|000001161
5|000007|Sun May 19 23:26:33 2002
yas.............................|/1..|pts/1...........................|000001156
6|000007|Sun May 19 23:21:56 2002
yas.............................|ts/0|pts/0...........................|000001153
4|000007|Sun May 19 23:21:49 2002
................................|....|pts/0...........................|000000286
3|000008|Sat May 18 19:20:57 2002
i000000.........................|ts/0|pts/0...........................|000000286
4|000007|Sat May 18 16:37:30 2002
................................|....|pts/0...........................|000000277
0|000008|Sat May 18 12:31:18 2002
% last | head -5 [←]
yas      pts/3                         Sun May 19 23:26 - 23:26  (00:00)    
yas      pts/2        bridge.hlla.is.t Sun May 19 23:26 - 23:26  (00:00)    
yas      pts/1                         Sun May 19 23:21   still logged in   
yas      pts/0        bridge.hlla.is.t Sun May 19 23:21   still logged in   
i000000  pts/0        dhcp06.xxlab.is. Sat May 18 16:37 - 19:20  (02:43)    
% []
----------------------------------------------------------------------
WTMP_FILE というファイルを、今回は、oepn() システムコールで開いている。 for 文で、i が -1 から -10 まで回している。offset = sizeof(struct utmp)*i で、ファイルの終わりからのバイト数を計算している。iはマイナス なので、ファイルの終わりからファイルのデータが先頭方向を意味する。 lseek(,offset,SEEK_END) で、ファイルの最後ら、マイナスの方向にシーク・ ポインタを移動している。そこの場所から、read() で、struct utmp の sizeof(entry)分だけデータを読み出し、utmp_print() で画面に表示している。 utmp_print() は、utmp-print.cと同じである。

◆fseek

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

■mmap

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

◆mmap-head.c


----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        mmap-head.c -- mmap(2) を使った head プログラム
   4:	        ~yas/syspro/file/mmap-head.c
   5:	        Start: 1998/05/12 01:06:15
   6:	*/
   7:	
   8:	#include <stdio.h>      /* stderr */
   9:	#include <sys/types.h>  /* open() */
  10:	#include <sys/stat.h>   /* open() */
  11:	#include <fcntl.h>      /* open() */
  12:	#include <unistd.h>     /* mmap() */
  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:	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:	}
----------------------------------------------------------------------

実行例。
----------------------------------------------------------------------
% cp ~yas/syspro/file/mmap-head.c . [←]
% make mmap-head [←]
cc     mmap-head.c   -o mmap-head
% ./mmap-head /etc/passwd [←]
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:
daemon:x:2:2:daemon:/sbin:
adm:x:3:4:adm:/var/adm:
lp:x:4:7:lp:/var/spool/lpd:
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:
news:x:9:13:news:/var/spool/news:
% []
----------------------------------------------------------------------

mmap-head.c は、head プログラムと同様に、最初の 10 行を画面に表示する ものである。関数 mmap_file() は、第1引数で指定された名前のファイルを メモリにマップし、その番地を返す。ファイルのバイト数を第2引数で指定さ れた番地に格納する。(ファイルがテキスト型でも、0で終端したりはしない。)

mmap_head() は、メモリ中の file_address 番地からsize バイトだけ、文字 の配列があるとみなしている。file_address[i] で、i バイト目にアクセスできる。 '\n' の数を数えて、10 に達したら抜ける。 そのバイト数まで、file_address 番地から一気に write() で標準出力に 書き出している。

■練習問題

★練習問題 28 3個のプロセスをパイプで結ぶ

3個のプロセスの標準入出力を、2つのパイプで結びなさい。

先にパイプを2つ作ってから2回 fork() してもよい。パイプを1つ作って fork() してから もう1つパイプを作って fork() するという方法でもよい。

ヒント: 使わないパイプのファイル記述子は、全部 close() すること。パイプの書き 込み側のファイル記述子が開いている間は、read() しても EOF (End Of File) にならない。自分自身で write() する可能性もあることに注意する。

★練習問題 29 書き手がいないパイプ

書き手(write()するプロセス)がいないパイプから読み出そうとすると、どう なるか確かめなさい。

ヒント:書き手がいないパイプは、全ての書込み側のファイル記述子を closeすると作れる。

★練習問題 30 読み手がいないパイプ

読み手がいないパイプに書き込むとどうなるか確かめなさい。

★練習問題 31 パイプ用のバッファの大きさ

各パイプには、オペレーティング・システムのカーネルの中にバッファが割り 当てられてている。この大きさを調べるプログラムを作りなさい。

ヒント:プロセスを fork() しなくても、パイプに書くことはできる。プロセ スが1個の状態で、バッファの大きさ以上のデータを書き込むと、プロセスが ブロックされる(先に進まなくなる)。引数で与えて与えられたバイト数だけ パイプに書込むプログラムを作り、何バイトからブロックされて止まるかを調べる。 止まったプログラムは、^C で強制終了させる。

1バイトずつ増やしていくのではなくて、2分探索をつかいなさい。大きいバ イト数から始める。止まれば、バイト数を半分にする。動けば、今のバイト数 と前にとまったバイト数の中間のバイト数にする。

★練習問題 32 読み手が複数いるパイプ

1つのパイプに読み手(read() するプロセス)が複数いた場合、どうなるか。 逆に、書き手が複数いた場合、どうなるか。

★練習問題 33 popen() ライブラリ関数

プロセスを実行してその結果を得るには、popen() ライブラリ関数が便利であ る。
----------------------------------------------------------------------
     FILE *popen(const char *command, const char *type);
     int pclose (FILE *stream);
----------------------------------------------------------------------
これを利用して、プロセスを作成し、プロセスにデータを与えたり、あるいは、 プロセスからデータを受け取るプログラムをつくりなさい。

たとえば、expr コマンドを実行して、その結果を受け取るプログラムをつく りなさい。expr は、次のように引数で与えられた式を評価し、結果を標準出 力に返すものである。


----------------------------------------------------------------------
% expr 1 + 2 [←]
3
% []
----------------------------------------------------------------------

この課題では、expr コマンドに似たようなプログラム作るのではなく、それ をそのまま利用して、結果を受け取るプログラムを作る。実行するプログラム としては、expr 以外に次のようなものが考えられる。

★練習問題 34 wakeupコマンド

次のような仕様を持つプログラムを作成しなさい。

% ./wakeup  [←]
Usage: % hh mm 
% ./wakeup 13 00 [←]
13:00
<13:00になったらbeep音を鳴らして終了する。>
% []
その main() 関数の例を以下に示す。

----------------------------------------------------------------------
   1:	/*
   2:	        wakeup.c -- 目覚まし時計
   3:	        ~yas/syspro/time/wakeup.c
   4:	        Start: 2003/05/12 00:59:43
   5:	*/
   6:	
   7:	#include <time.h>       /* mktime */
   8:	#include <stdio.h>      /* stderr() */
   9:	
  10:	void wakeup( int hh, int mm );
  11:	
  12:	main( int argc, char *argv[])
  13:	{
  14:	    int hh, mm ;
  15:	
  16:	        if( argc != 3 )
  17:	        {
  18:	            fprintf(stderr,"Usage: %% hh mm \n",argv[0] );
  19:	            exit( 1 );
  20:	        }
  21:	        hh = strtol( argv[1],0,10 );
  22:	        mm = strtol( argv[2],0,10 );
  23:	        wakeup( hh, mm );
  24:	        exit( 0 );
  25:	}
  26:	
  27:	void wakeup( int hh, int mm )
  28:	{
  29:	    int n ;
  30:	        printf("%02d:%02d\n", hh, mm );
  31:	        n = 3 ;
  32:	        sleep( n );
  33:	        write(0,"\a",1);
  34:	}
----------------------------------------------------------------------

この wakeup() では、単に定数 3 を指定しているので、3秒間だけsleep し た後、画面に \a を出力することで音を鳴らしている。この課題では、定数で はなく、現在からの目的の時刻までの差を調べ、その秒数だけ sleep させる ようにする。

目的の時刻を得るには、次のようにする方法がある。

  1. 現在の時刻を time(2) や gettimeofday(2) で求める。
  2. 1. で得られた時刻を、localtime(3) で struct tm に変換する。
  3. 2. で得られた struct tm のうち、時、分、秒の部分を修正する。
  4. 3. の結果を mktime(3) で time_t にする。
  5. 4. の結果と、現在の時刻の差を求める。

★練習問題 35 dateコマンド

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

プログラムは、3行程度になる。日本語表示でなくてもよい。Linux のdate コマンドの日本語表示は、おかしいので、それまで真似する必要はない。独自 に改良するとよい。

この課題では、ctime(3) を用いてはならない。 strftime(3)、または、localtime(3) を使ってもよい。

★練習問題 36 whoコマンド

who コマンドと似たプログラム(現在ログインしているユーザを表示するプログラム)を 作りなさい。

utmp-print.cから出発する。ut_type が、 USER_PROCESS のものだけを、表示する。ただし、表示は、単に utmp_print() を呼ぶのではなく、本物の who コマンドにできるだけ似せなさい。

ut_time の表示には、ctime() を使ってもよいが、できれば、localtime() や strftime() を使って分単位で短く表示する。

★練習問題 37 lastコマンド

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

wtmp-last10.c のプログラムから出発する。ロ グアウトの情報を、どこかに記録する。それとマッチするログインの情報が現 れた時に、表示する。

この課題では、wtmp にある情報だけが表示できればよい。

★練習問題 38 ローカル・ログイン判定プログラム

この講義で用いている [出席ボタン] には、遠隔ログインされたり、proxy を使われた時にホスト名を詐称される弱 点がある。それを補うようなプログラムを作成しなさい。たとえば、次の表示 を考える。
----------------------------------------------------------------------
host: 130.158.86.51 , time: 2003-04-21-12-23-56, 2
----------------------------------------------------------------------
この時刻に、このホストで、ローカルにログインしていた場合、そのホストで プログラムを実行すると、0 を返す( exit(0) で終了する)。そうでない場合、 1 を返す( exit(1) で終了する)。

% ./llogin user1 2003-04-21-12-23-56 [←]
% echo $status  [←]
0
% ./llogin user1 2003-04-21-12-23-56 && echo ok [←]
ok
% []
引数として、wtmp ファイル(複数)を取れるようにすることが望ましい。(wtmp ファイルは、1ヶ月ごとに消されるが、消される前にホストごとに収集してあ る。)

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

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

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


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

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

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

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

mmap() を使った head

を参考 にして、tail に変えるとよい。lseek() や fseek() を使ったものでは、やり にくいかもしれない。