システム・コール、ライブラリ、ファイル、プロセス

システム・プログラム

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

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

■今日の目標

ファイルの考え方と操作 プロセスの考え方と操作

■講義の目的

システム・プログラムでは、オペレーティング・システムが提供する次のよう な機能や考え方を理解することを目的とする。

このような基本的な考え方は、時間が経過してもそれほど変わらない。 この講義で得られた知識は、10年後も十分通用する。

オペレーティング・システムの「内部の動き」は、2学期の「オペレーティン グ・システム」という講義で扱う。内部の動きの前に、外から眺めてみて、オ ペレーティング・システムの考え方を理解することを目的とする。 3学期には、さらに「オペレーティング・システム II 」、「分散システム」 で最近の話題に繋がる。

この講義では、Unix の API (Application Program Interface) を利用し てプログラムを作成する。API は、次の3つに分類される。

なぜ API が重要か

この講義で扱う Unix の API は、他のものと比較すると簡単になっている。 複雑なものには、Xウインドウ・システム、MS-Windows (Win32 API) などが ある。

「雰囲気」から「仕組みの理解」へ。

■コンピュータ・システムの構成

■オペレーティング・システムの構成要素

UNIXオペレーティング・システムは、カーネル、サーバ、シェ ル、コマンド群、各種ライブラリ関数、といった要素から構成さ れている。

図? オペレーティング・システムの構成要素

図? オペレーティング・システムの構成要素

プログラムの活動 システムコールは、カーネルに仕事を依頼する。入出力を行う場合は、必ずシ ステムコールになる(mmapされたメモリの書き換えなど、一部例外がある)。 ライブラリ関数は、プロセス内のメモリ中のデータを書き換えるか、最終的に はシステムコールを呼び出して入出力を行う。

■プログラムのコンパイルと実行

3種類のプログラム コンパイルから実行までに必要なプログラム

図? ソース・プログラムから実行形式まで

図? ソース・プログラムから実行形式まで

cc -c で実行されるプログラム
cpp(the C language preprocessor)
プリプロセッサ(前処理プログラム、マクロプロセッサ)
cc1(C Compiler)
Cコンパイラ本体
as(Assembler)
アセンブラ
assembly language で書かれたプログラムを assembler でオブジェクト・プ ログラムに変換する。

図? cc -c が実行するプログラム

図? cc -c が実行するプログラム

コンパイルの観察。

◆cc


% nl -ba today.c [←]
     1  #define TODAY   "Monday"
     2  main()
     3  {
     4          printf("Today is %s.\n",TODAY );
     5  }
% cc today.c [←]
% ./a.out  [←]
Today is Monday.
% []

◆cc -v


% cc -v today.c  [←]
Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/2.96/specs
gcc version 2.96 20000731 (Red Hat Linux 7.1 2.96-85)
 /usr/lib/gcc-lib/i386-redhat-linux/2.96/cpp0 -lang-c -v -D__GNUC__=2 -D__GNUC_MINOR__=96 -D__GNUC_PATCHLEVEL__=0 -D__ELF__ -Dunix -Dlinux -D__ELF__ -D__unix__ -D__linux__ -D__unix -D__linux -Asystem(posix) -D__NO_INLINE__ -Acpu(i386) -Amachine(i386) -Di386 -D__i386 -D__i386__ -D__tune_i386__ today.c /tmp/ccBBSvXh.i
GNU CPP version 2.96 20000731 (Red Hat Linux 7.1 2.96-85) (cpplib) (i386 Linux/ELF)
ignoring nonexistent directory "/usr/i386-redhat-linux/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/include
 /usr/lib/gcc-lib/i386-redhat-linux/2.96/include
 /usr/include
End of search list.
 /usr/lib/gcc-lib/i386-redhat-linux/2.96/cc1 /tmp/ccBBSvXh.i -quiet -dumpbase today.c -version -o /tmp/cc8l7Fmp.s
GNU C version 2.96 20000731 (Red Hat Linux 7.1 2.96-85) (i386-redhat-linux) compiled by GNU C version 2.96 20000731 (Red Hat Linux 7.1 2.96-85).
 as -V -Qy -o /tmp/ccB4lWuy.o /tmp/cc8l7Fmp.s
GNU assembler version 2.10.91 (i386-redhat-linux) using BFD version 2.10.91.0.2
 /usr/lib/gcc-lib/i386-redhat-linux/2.96/collect2 -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crt1.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crti.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/crtbegin.o -L/usr/lib/gcc-lib/i386-redhat-linux/2.96 -L/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../.. /tmp/ccB4lWuy.o -lgcc -lc -lgcc /usr/lib/gcc-lib/i386-redhat-linux/2.96/crtend.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crtn.o
% []

◆cc -E

プリプロセッサだけ動かして、結果を標準出力へ出す。

% cc -E today.c > today.i [←]
% nl -ba today.i [←]
     1  # 2 "today.c"
     2  main()
     3  {
     4          printf("Today is %s.\n","Monday" );
     5  }
% []

◆cc -S

アセンブラを実行しないで、アセンブリ言語のプログラムを .s ファイルに残 す。

◆リンクの観察

リンクは、ld というプログラムで行われることが多い(下は違う)。

% cc -c today.c [←]
% ls -l today.o [←]
-rw-r--r--    1 yas      lab           932  4月 14 17:11 today.o
% nm today.o [←]
00000000 t gcc2_compiled.
00000000 T main
         U printf
% cc -v -o a.out today.o [←]
Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/2.96/specs
gcc version 2.96 20000731 (Red Hat Linux 7.1 2.96-85)
 /usr/lib/gcc-lib/i386-redhat-linux/2.96/collect2 -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o a.out /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crt1.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crti.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/crtbegin.o -L/usr/lib/gcc-lib/i386-redhat-linux/2.96 -L/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../.. today.o -lgcc -lc -lgcc /usr/lib/gcc-lib/i386-redhat-linux/2.96/crtend.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crtn.o
% []

様々なライブラリ(-lgcc -lc -lgcc) が参照されている。

■システム・コールとライブラリ関数

C言語でプログラムを作る時に、次の3つを使うことになる。 システム・コールの例
open(),close(),read(),write()
ライブラリ関数の例
fopen(),fclose(),fread(),fwrite(),printf(),scanf(),getchar(),putchar()
マクロ定義の例
getc(),putc(),isalpha(),isdigit(),
(stdin,stdout,stderr,getchar(),putchar() がマクロにもなっているシステムも多い。)

ヘッダ・ファイル stdio.h の観察

% egrep getc /usr/include/stdio.h  [←]
extern int fgetc (FILE *__stream) __THROW;
extern int getc (FILE *__stream) __THROW;
extern int getchar (void) __THROW;
#define getc(_fp) _IO_getc (_fp)
extern int getc_unlocked (FILE *__stream) __THROW;
extern int getchar_unlocked (void) __THROW;
extern int fgetc_unlocked (FILE *__stream) __THROW;
extern int ungetc (int __c, FILE *__stream) __THROW;
% cat getchar.c [←]
#include 

main()
{
        printf("%c\n",getc(stdin) );
        printf("%c\n",getchar() );
}
% cc -E getchar.c >  getchar.i [←]
% tail -5 getchar.i [←]
main()
{
        printf("%c\n",_IO_getc (stdin) );
        printf("%c\n",getchar() );
}
% []

◆ファイルのコピー(システムコールの利用)

file-copy.c は、システムコール open(), read(), write(), close() を使っ て引数で指定されたファイルを開き、その内容をコピーするプログラムである。
   1:	
   2:	/*
   3:	        file-copy.c -- ファイルをコピーする簡単なプログラム
   4:	        ~yas/syspro/file/file-copy.c
   6:	        Start: 1995/03/04 16:40:24
   7:	*/
   8:	
   9:	#include <stdio.h>      /* stderr */
  10:	#include <fcntl.h>      /* open(2) */
  11:	
  12:	extern void file_copy( char *from_name, char *to_name );
  13:	
  14:	main( int argc, char *argv[] )
  15:	{
  16:	        if( argc != 3 )
  17:	        {
  18:	            fprintf( stderr,"Usage: %s from to\n", argv[0] );
  19:	            exit( 1 );
  20:	        }
  21:	        file_copy( argv[1],argv[2] );
  22:	}
  23:	
  24:	#define BUFFERSIZE      1024
  25:	
  26:	void file_copy( char *from_name, char *to_name )
  27:	{
  28:	    int from_fd,to_fd ;
  29:	    char buffer[BUFFERSIZE] ;
  30:	    int rcount ;
  31:	    int wcount ;
  32:	
  33:	        from_fd = open( from_name,O_RDONLY );
  34:	        if( from_fd == -1 )
  35:	        {
  36:	            perror( from_name );
  37:	            exit( 1 );
  38:	        }
  39:	        to_fd = open( to_name,O_WRONLY|O_CREAT|O_TRUNC,0666 );
  40:	        if( to_fd == -1 )
  41:	        {
  42:	            perror( to_name );
  43:	            exit( 1 );
  44:	        }
  45:	        while( (rcount=read(from_fd,buffer,BUFFERSIZE)) > 0 )
  46:	        {
  47:	            if( (wcount=write(to_fd,buffer,rcount))!= rcount )
  48:	            {
  49:	                 perror( to_name );
  50:	                 exit( 1 );
  51:	            }
  52:	        }
  53:	        close( from_fd );
  54:	        close( to_fd ); 
  55:	}

プログラムに引数を渡すことができる。argv[0] には、プログラムの名前を示 す文字列が入っている。自分のプログラムの名前で動作を変える時にだけ参照 する。argv[1] 以降が本当の意味での引数である。argc には、argv[0]を含め た引数の数が入っている。

open()は、ファイルを開くシステム・コールである。O_RDONLYとは、 読み込み専用でファイルを開くことを意味している。open() システム・コー ルは、結果としてファイル記述子を返す。ファイル記述子は、負でない小さな 整数(だいたい0〜1024の範囲、最大はシステムによって違う)である。 ファイル記述子は、read()システム・コールやwrite()システム・コールで実 際にファイルを読み書きする時に使われる。

エラーが起きた時には、open() システム・コールは、-1 を返す。この時、エ ラーの番号が errno という変数に格納される。perror() は、errno変数を解 析して、より詳しいエラー・メッセージを表示する関数である。perror() の 引数は、エラー・メッセージとともに表示される文字列である。

ここでは、出力用のファイルを開いている。O_WRONLYは、書き込み専用でファ イルを開くことを意味している。O_CREATは、ファイルが存在しなければ作る ように指示するものである。ファイルが存在する場合、上書きされる。 O_TRUNCは、ファイルが存在している時には、その大きさを0にすることを指 示するものである。0666 (C言語で0から始まる数は、8進数)は、ファイルを 作る時のモードである。この数値に従って、ファイルのモード(ls -l で  rwxrwxrwx と表示される部分)が決定される。作成されるファイルのモードは、 ここで指定されたモードから現在の umask が落とされた(引かれた)値とな る。

read() システム・コールは、第1引数で指定されたファイル記述子のファイ ルを読み込み、それを第2引数の番地へ保存する。読み込むバイト数は、第3 引数で与えられる。結果として読み込んだバイト数を返す。通常は、BUFFERSIZE が返される。read() システム・コールの結果、ファイル上の読み書きする位 置が、実際に読み込んだバイト数だけずれる。ファイルの末尾近くや、ファイ ルが端末の時、BUFFERSIZE 以下の値が返される。ファイルの末尾に行き着くと  0 が返される。エラーが起きると、-1 が返される。

write() システム・コールは、第1引数で指定されたファイル記述子のファイ ルへデータを書き込む。書き込まれるデータは、第2引数で与えられた番地か ら、第3引数で与えらた大きさである。write() システム・コールは、結果と して書き込んだバイト数を返す。ファイル上の読み書きする位置が、実際に読 み込んだバイト数だけずれる。通常は、BUFFERSIZE が返される。空き容量不足 などで書き込みが失敗した時には、-1 を返す。エラーが起きると、-1 が返さ れる。

close() は、ファイルを閉じるシステム・コールである。このシステム・コールを実行してしまうと、そのファイル記述子は無効になる。すなわち、read() や write() を行うと、エラーになる。

% mkdir file [←]
% cd file [←]
% cp ~yas/syspro/file/file-copy.c . [←]
% make file-copy [←]
cc     file-copy.c   -o file-copy
% ls [←]
file-copy    file-copy.c
% echo "This is file1" > file1 [←]
% cat file1 [←]
This is file1
% ls [←]
file-copy  file-copy.c  file1
% ./file-copy file1 file2 [←]
% ls [←]
file-copy    file-copy.c  file1        file2
% ls -l [←]
総ブロック数 30
-rwxr-xr-x    1 yas      lab        12616   4月 22日 17時19分  file-copy
-rw-r--r--    1 yas      lab         1096   4月 22日 17時19分  file-copy.c
-rw-r--r--    1 yas      lab           14   4月 22日 17時20分  file1
-rw-r--r--    1 yas      lab           14   4月 22日 17時20分  file2
% diff file1 file2 [←]
% cmp file1 file2 [←]
% cat file2 [←]
This is file1
% []
strace コマンドを使うと、どのようなシステムコールが使われているかがわ かる。strace コマンドは、Linux, FreeBSD 等で使える。System V 系では、 truss, BSD 系では、ktrace が使える。
% strace ./file-copy file1 file2 [←]
execve("./file-copy", ["./file-copy", "file1", "file2"], [/* 52 vars */]) = 0
uname({sys="Linux", node="adonis9.coins.tsukuba.ac.jp", ...}) = 0
brk(0)                                  = 0x80498c8
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40016000
open("/etc/ld.so.preload", O_RDONLY)    = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=87112, ...}) = 0
old_mmap(NULL, 87112, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000
close(3)                                = 0
open("/lib/libc.so.6", O_RDONLY)        = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0 \304\1"..., 1024) = 1024
fstat64(3, {st_mode=S_IFREG|0755, st_size=5737218, ...}) = 0
old_mmap(NULL, 1267240, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x4002d000
mprotect(0x40159000, 38440, PROT_NONE)  = 0
old_mmap(0x40159000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x12b000) = 0x40159000
old_mmap(0x4015f000, 13864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x4015f000
close(3)                                = 0
munmap(0x40017000, 87112)               = 0
open("file1", O_RDONLY)                 = 3
open("file2", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
read(3, "This is file1\n", 1024)        = 14
write(4, "This is file1\n", 14)         = 14
read(3, "", 1024)                       = 0
close(3)                                = 0
close(4)                                = 0
_exit(0)                                = ?
% []

◆ファイル記述子

ファイル記述子は、小さな整数である。
   1:	/*
   2:	        fd-print.c -- ファイル記述子を表示するプログラム
   3:	        ~yas/syspro/file/fd-print.c
   5:	        Start: 1997/04/21 18:23:13
   6:	*/
   7:	
   8:	#include <fcntl.h>
   9:	
  10:	void main( int argc, char *argv[] )
  11:	{
  12:	    int i ;
  13:	    int fd ;
  14:	        for( i=1 ; i<argc ; i++ )
  15:	        {
  16:	            fd = open( argv[i],O_RDONLY );
  17:	            printf("%s: %d\n",argv[i],fd );
  18:	        }
  19:	}
実行例:
% ./fd-print fd-print.c  [←]
fd-print.c: 3
% ./fd-print fd-print.c /etc/passwd fd-print.c [←]
fd-print.c: 3
/etc/passwd: 4
fd-print.c: 5
% []

◆標準入出力を利用したファイルのコピー(システムコール)

プログラムが実行される時、open() システム・コールを使わなくても、次の 3つのファイル記述子は使える状態になっている。

0
標準入力(standard input)
1
標準出力(standard output)
2
標準エラー出力(standard error)

stdio-thru.cは、標準入力で指定されたファイルの内容を標準出力で指定され たファイルへコピーするプログラムである。

   1:	
   2:	/*
   3:	        stdio-thru.c -- 標準入力から標準出力へのコピー
   4:	        ~yas/syspro/file/stdio-thru.c
   6:	        Start: 1995/03/04 16:40:24
   7:	*/
   8:	
   9:	#include <stdio.h>
  10:	
  11:	extern void stdio_thru(void);
  12:	
  13:	main( int argc, char *argv[] )
  14:	{
  15:	        if( argc != 1 )
  16:	        {
  17:	            fprintf( stderr,"Usage: %s\n", argv[0] );
  18:	            exit( 1 );
  19:	        }
  20:	        stdio_thru();
  21:	}
  22:	
  23:	#define BUFFERSIZE      1024
  24:	
  25:	void stdio_thru()
  26:	{
  27:	    char buffer[BUFFERSIZE] ;
  28:	    int rcount ;
  29:	    int wcount ;
  30:	
  31:	        while( (rcount=read(0,buffer,BUFFERSIZE)) > 0 )
  32:	        {
  33:	            if( (wcount=write(1,buffer,rcount))!= rcount )
  34:	            {
  35:	                 perror("stdout");
  36:	                 exit( 1 );
  37:	            }
  38:	        }
  39:	        close( 0 );
  40:	        close( 1 );
  41:	}
このプログラムは、引数をとらない。"<" や ">" は、シェルにより解釈され、 このプログラムには渡されない。

file_copy() とは異なり、stdio_thru() では、ファイルを開く操作(open()) を行うことなく、入出力(read(),write)を行っている。このような事が可能 な理由は、UNIXでは、ファイル記述子 0, 1, 2 は、シェルにより既に開 かれているからである。

ファイル記述子 0 は、なにもしないと端末のキーボードに割り当てられてい る。ファイル記述子 1, 2 は、端末の画面に割り当てられている。しかし、 "<"、">"、"|"を使うと別のファイル やパイプに切替えられる。

% ./stdio-thru [←]
ljd[←]
ljd
sk[←]
sk
^D
% ls bbb [←]
bbb がアクセスできません: そのようなファイルまたはディレクトリはありません
% ./stdio-thru < stdio-thru.c > bbb [←]
% ls -l stdio-thru.c bbb [←]
-rw-r--r--    1 yas      lab          690   4月 22日 17時37分  bbb
-rw-r--r--    1 yas      lab          690   4月 22日 17時35分  stdio-thru.c
% []

◆ファイルのコピー(ライブラリの利用)

open(), close(), read(), write() の変りに fopen(), fclose(), fread(), fwrite() を使えばよい。

プログラムは、~yas/syspro/file/file-copy-lib.c にある。

   1:	
   2:	/*
   3:	        file-copy.c -- ファイルをコピーする簡単なプログラム(ライブラリ)
   4:	        ~yas/syspro/file/file-copy-lib.c
   5:	        Created on 2004/04/11 17:31:29
   6:	*/
   7:	
   8:	#include <stdio.h>      /* stderr, fopen(), fread(), frwite(), fclose() */
   9:	#include <fcntl.h>      /* open(2) */
  10:	
  11:	extern void file_copy_lib( char *from_name, char *to_name );
  12:	
  13:	main( int argc, char *argv[] )
  14:	{
  15:	        if( argc != 3 )
  16:	        {
  17:	            fprintf( stderr,"Usage: %s from to\n", argv[0] );
  18:	            exit( 1 );
  19:	        }
  20:	        file_copy_lib( argv[1],argv[2] );
  21:	}
  22:	
  23:	#define BUFFERSIZE      1024
  24:	
  25:	void file_copy_lib( char *from_name, char *to_name )
  26:	{
  27:	    FILE *from_file, *to_file ;
  28:	    char buffer[BUFFERSIZE] ;
  29:	    int rcount ;
  30:	    int wcount ;
  31:	
  32:	        from_file = fopen( from_name,"r" );
  33:	        if( from_file == NULL )
  34:	        {
  35:	            perror( from_name );
  36:	            exit( 1 );
  37:	        }
  38:	        to_file = fopen( to_name,"w" );
  39:	        if( to_file == 0 )
  40:	        {
  41:	            perror( to_name );
  42:	            exit( 1 );
  43:	        }
  44:	        while( (rcount=fread(buffer,1,BUFFERSIZE,from_file)) > 0 )
  45:	        {
  46:	            if( (wcount=fwrite(buffer,1,rcount,to_file))!= rcount )
  47:	            {
  48:	                 perror( to_name );
  49:	                 exit( 1 );
  50:	            }
  51:	        }
  52:	        fclose( from_file );
  53:	        fclose( to_file );      
  54:	}
実行例
% cp ~yas/syspro/file/file-copy-lib.c . [←]
% make file-copy-lib [←]
cc     file-copy-lib.c   -o file-copy-lib
% ./file-copy-lib file1 file3 [←]
% cat file3 [←]
This is file1
% []
strace コマンドで調べると、同じシステムコールを使っていることがわかる。
% strace ./file-copy-lib file1 file3 [←]
execve("./file-copy-lib", ["./file-copy-lib", "file1", "file3"], [/* 52 vars */]) = 0
...
brk(0x804a000)                          = 0x804a000
open("file1", O_RDONLY)                 = 3
open("file3", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 4
fstat64(3, {st_mode=S_IFREG|0644, st_size=14, ...}) = 0
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000
read(3, "This is file1\n", 4096)        = 14
read(3, "", 4096)                       = 0
fstat64(4, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40018000
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0x40017000, 4096)                = 0
write(4, "This is file1\n", 14)         = 14
close(4)                                = 0
munmap(0x40018000, 4096)                = 0
_exit(0)                                = ?
% []
Linux では、ltrace コマンドを使うと、どのようなライブラリ関数が使われ ているかがわかる。

% ltrace ./file-copy-lib file1 file3 [←]
__libc_start_main(0x080485e0, 3, 0xbfffe604, 0x080483fc, 0x08048790 
__register_frame_info(0x080497e0, 0x080498d4, 0xbfffe5a8, 0x0804850e, 0x080483fc) = 0x4015e460
fopen("file1", "r")                               = 0x080498f8
fopen("file3", "w")                               = 0x08049a68
fread(0xbfffe160, 1, 1024, 0x080498f8)            = 14
fwrite("This is file1\n", 1, 14, 0x08049a68)      = 14
fread(0xbfffe160, 1, 1024, 0x080498f8)            = 0
fclose(0x080498f8)                                = 0
fclose(0x08049a68)                                = 0
__deregister_frame_info(0x080497e0, 0x4000c981, 0x400164c4, 0x40016618, 1) = 0x080498d4
+++ exited (status 0) +++
% []

◆文字単位のフィルタ(ライブラリ)

ライブラリ関数は、文字単位の入出力や行単位の入出力、書式付の出力に使う のが本来の使い方である。
   1:	/*
   2:	        filter-char.c -- 文字単位の簡単なフィルタ
   3:	        ~yas/syspro/file/filter-char.c
   5:	        Start: 1997/04/21 18:23:13
   6:	*/
   7:	
   8:	#include <stdio.h>      /* stdin, stdout, fopen(), fclose(), getc(), */
   9:	#include <ctype.h>      /* toupper() */
  10:	
  11:	extern void filter_char_upper( FILE *in, FILE *out );
  12:	
  13:	main( int argc, char *argv[] )
  14:	{
  15:	    int i ;
  16:	    FILE *fp ;
  17:	        if( argc == 1 )
  18:	            filter_char_upper( stdin, stdout );
  19:	        else
  20:	        {
  21:	            for( i=1 ; i< argc ; i++ )
  22:	            {
  23:	                if( (fp = fopen( argv[i],"r" )) == NULL )
  24:	                {
  25:	                    perror( argv[i] );
  26:	                    exit( -1 );
  27:	                }
  28:	                filter_char_upper( fp, stdout );
  29:	                fclose( fp );
  30:	            }
  31:	        }
  32:	}
  33:	
  34:	void filter_char_upper( FILE *in, FILE *out )
  35:	{
  36:	    int c ;     /* int型。char は、不可 */
  37:	        while( (c=getc(in)) != EOF )
  38:	        {
  39:	            putc( toupper(c),out );
  40:	        }
  41:	}

fopen()は、ファイルを開くライブラリ関数である。"r" は、 読み込み専用で ファイルを開くことを意味している。fopen()は、結果として FILE 構造体へ のポインタを返す。これは、次のようなライブラリ関数で使われる。

    fgetc()           fputs()           getc()         getw()
    fgets()           fread()           ungetc()
    fprintf()         fscanf()
    fputc()           fwrite()          putc()         putc()
    fflush()          fseek()           ftell()
    frewind()         fclose()          fdopen()       freopen()
    ferrror()         feof()            clearerr()     fileno()

このような、FILE * を使うライブラリ関数群は、次のような名前で呼ば れる。

fread(), fwrite() は、ある固定長の構造体を入出力するための関数である。 size_t fread( void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite( const void *ptr, size_t size, size_t nmemb, FILE *stream); 結果として、size * nmemb バイトのデータが入力、または、出力される。 size に 1 (sizeof(char))を指定するのは、本来の使い方ではない。 本来は、sizeof(struct xxx) のように固定長の構造体の大きさを指定する。

fclose() は、ファイルを閉じるライブラリ関数である。

実行例

% ./filter-char [←]
abc[←]
ABC
asdfjkl;[←]
ASDFJKL;
^D
% []

putc() は、第1引数で指定された文字を第2引数で与えられたストリームへ 出力する。

◆ファイルのコピー(Java)

ブロック単位での入出力ならば、Java では、FileInputStream() や FileOutputStream() を使う。
   1:	/*
   2:	        file-copy.c -- ファイルをコピーする簡単なプログラム(ライブラリ)
   3:	        ~yas/syspro/file/file-copy-lib.c
   4:	        Created on 2004/04/11 18:14:52
   5:	 */
   6:	
   7:	import java.io.*;
   8:	
   9:	public class FileCopy
  10:	{
  11:	    static java.io.BufferedReader stdin = 
  12:	        new java.io.BufferedReader( new java.io.InputStreamReader(System.in) );
  13:	    static java.io.PrintStream stdout = System.out;
  14:	    static java.io.PrintStream stderr = System.err;     
  15:	
  16:	    public static void main(String argv[])
  17:	    {
  18:	        if( argv.length != 2 )
  19:	        {
  20:	            stderr.println("Usage: % java FileCopy from to");
  21:	            System.exit( 1 );
  22:	        }
  23:	        file_copy( argv[0], argv[1] );
  24:	    }
  25:	
  26:	    static int BUFFERSIZE = 1024 ;
  27:	
  28:	    public static void file_copy( String from, String to )
  29:	    {
  30:	        try
  31:	        {
  32:	            InputStream from_is = new FileInputStream( from );
  33:	            OutputStream to_os = new FileOutputStream( to );
  34:	            byte buffer[] = new byte[BUFFERSIZE];
  35:	            int rcount;
  36:	            while( (rcount=from_is.read(buffer))>=0 )
  37:	            {
  38:	                to_os.write( buffer,0,rcount );
  39:	            }
  40:	            from_is.close();
  41:	            to_os.close();
  42:	        }
  43:	        catch( Exception e )
  44:	        {
  45:	            stderr.println( e );
  46:	        }
  47:	    }
  48:	}
実行例
% cp ~yas/syspro/file/FileCopy.java . [←]
% javac FileCopy.java [←]
% ls [←]
FileCopy.class  file-copy      file-copy-lib.c  file1  file3
FileCopy.java   file-copy-lib  file-copy.c      file2
% java FileCopy  [←]
Usage: % java FileCopy from to
% java FileCopy file1 file4 [←]
% ls [←]
FileCopy.class  file-copy      file-copy-lib.c  file1  file3
FileCopy.java   file-copy-lib  file-copy.c      file2  file4
% cat file1 [←]
This is file1
% cat file4 [←]
This is file1
% []
strace コマンドで調べると、同じシステムコールを使っていることがわかる。
% strace java FileCopy file1 file4 [←]
execve("/usr/java/j2sdk1.4.1_02/bin/java", ["java", "FileCopy", "file1", "file4"], [/* 52 vars */]) = 0
...
open("file1", O_RDONLY|O_LARGEFILE)     = 5
fstat64(5, {st_mode=S_IFREG|0644, st_size=14, ...}) = 0
open("file4", O_WRONLY|O_CREAT|O_TRUNC|O_LARGEFILE, 0666) = 6
fstat64(6, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
read(5, "This is file1\n", 1024)        = 14
write(6, "This is file1\n", 14)         = 14
read(5, "", 1024)                       = 0
close(5)                                = 0
close(6)                                = 0
...

◆ライブラリとシステムコールの混在

普通、同じファイルについては、ライブラリとシステム・コールを混ぜて使う ことはしない。たとえば、read(0,,) と getc(stdin)、write(1,,) と fprintf(stdout,,) を混ぜない。ライブラリは、バッファリングしているので、 タイミングが合わなくなる。

特殊な状況では、混ぜる必要がでてくる。 その時は、次のような関数を使う。

fileno()
FILE * で使われているファイル記述子を調べる。
fdopen()
ファイル記述子から FILE * を作る。
fflush()
FILE * の内部のバッファに溜っているデータを書き出す。

◆キーボード入力のバッファリング

システムコールでも、キーボードからの入力ではバッファリングが 行われる。getchar() は、リターンが押されるまで、1文字も変えさない。 それまでは、バックスペースなどで編集することもできる。

この機能を抑止するには、stty cbreak とするシステムが多い。 (Irix など、cbreak が使えないシステムもある。)

% cat [←]
hello[←]
hello
^D
% stty cbreak;cat [←]
hheelllloo[←]

^C
% []
stty raw でも、バッファリングがなくなるが、^C による強制終了もできなく なるので、注意する。

% cat [←]
abc[←]
abc
^D
% stty raw; cat [←]
aabbcc

			(^Dが利かない)
			(別の端末から ps して kill)
Terminated
% []

■プロセスとは

◆プログラムとプロセス

プロセッサ(CPU)が実行できる機械命令の列がプログラムである。 プロセスとは、プログラムがオペレーティング・システムによってメモリに読 み込まれ、やはりオペレーティング・システムの管理下にあるプロセッサによっ て実行の対象になったものである。

メモリにプロセスが3つ、UNIXカーネル、ハードディスク、CPU

図? プログラムとプロセス

プロセスの機能
保護
1つのプロセスが暴走しても他のプロセスやオペレーティング・システ ムには影響を与えない。
資源割り当て
資源とは、メモリ、ディスク、プリンタ、ディスプレイ、キーボードな どコンピュータが処理を進める上で利用価値のあるものを総称である。プロセ スは、資源を公平に配分するための単位としての役割がある。

◆プロセスの操作

プロセスの操作には次のようなものがある。

◆プロセスの親子関係

Unix では、後で説明する fork() システムコールを 発行すると、新しくプロセスが作られる。 (Unix では、これ以外の方法ではプロセスは作られない。) この時、「もとのプロセス」の「親プロセス」という。 プロセスの親プロセスのプロセス識別子は、psコマンドに「-l」オプションを つけるとPPIDのところに表示されている。

◆プロセスの資源と属性

◆ps コマンドとプロセスの強制終了

■mainの引数

既にfile-copy.cでも出 てきたが、プログラムを実行する時、コマンドラインから引数を渡すことがで きる。
   1:	/*
   2:	        arg-print.c -- mainの引数を表示するプログラム
   3:	        ~yas/syspro/proc/arg-print.c
   4:	        Start: 1997/04/21 18:23:13
   5:	*/
   6:	
   7:	main( int argc, char *argv[], char *envp[] )
   8:	{
   9:	    int i ;
  10:	        printf("&argc == 0x%x, argc == %d\n", &argc, argc );
  11:	        printf("&argv == 0x%x, argv == 0x%x\n",&argv, argv );
  12:	        for( i=0 ; argv[i] ; i++ )
  13:	            printf("argv[%d]==0x%x, \"%s\"\n",i,argv[i],argv[i] );
  14:	}
実行例。

% cp ~yas/syspro/proc/arg-print.c . [←]
% make arg-print [←]
cc     arg-print.c   -o arg-print
% ./arg-print  [←]
&argc == 0xbfffe7c0, argc == 1
&argv == 0xbfffe7c4, argv == 0xbfffe824
argv[0]==0xbffffa81, "./arg-print"
% ./arg-print who am i [←]
&argc == 0xbfffe740, argc == 4
&argv == 0xbfffe744, argv == 0xbfffe7a4
argv[0]==0xbffffa78, "./arg-print"
argv[1]==0xbffffa84, "who"
argv[2]==0xbffffa88, "am"
argv[3]==0xbffffa8b, "i"
% []
argv[0] には、プログラムの名前が含まれている。argv[1] 以降に、普通の意 味での引数が含まれている。argc には、プログラムの名前まで含めての引数 の数が含まれている。argv[0] からargv[argc-1] まで参照できる。 argv[argc] は、参照してはいけない(0が入っているはずではあるが)。

◆printf()の%xと%s

printf() の %x, %s の意味に注意する。
%x
4バイトの値を数だと思って、16進表記の文字コードの並び変換して表示する。
%s
4バイトの値を番地だと思って、その番地から始まる 内容を、0が来るまで表示する。

◆環境変数

main の引数以外にプログラムの動きを変えるものに、環境変数がある。環境 変数は、csh (tcsh) からは、setenv コマンドで設定できる。

% printenv LANG [←]
ja_JP.eucJP
% date [←]
日  4月 27 23:45:23 JST 2003
% setenv LANG C [←]
% date [←]
Sun Apr 27 23:45:29 JST 2003
% []
環境変数は、プログラムからは、main() の3番目の引数、外部変数 environ 、 ライブラリ関数 getenv() でアクセスできる。メモリ中の構造は、argv と同 じである。各文字列は、「変数名=値」の形式になっている。
   1:	/*
   2:	        env-print.c -- 環境変数を表示するプログラム
   3:	        ~yas/syspro/proc/env-print.c
   4:	        Start: 1997/05/05 16:42:22
   5:	*/
   6:	extern char **environ ;
   7:	
   8:	main( int argc, char *argv[], char *envp[] )
   9:	{
  10:	    int i ;
  11:	        printf("envp == 0x%x\n",envp );
  12:	        printf("environ == 0x%x\n",environ );
  13:	        for( i=0 ; envp[i] ; i++ )
  14:	            printf("envp[%d]==0x%x, \"%s\"\n",i,envp[i],envp[i] );
  15:	}

実行例。

% cp ~yas/syspro/proc/env-print.c . [←]
% make env-print [←]
cc     env-print.c   -o env-print
% ./env-print  [←]
envp == 0xbfffe5bc
environ == 0xbfffe5bc
envp[0]==0xbffffa93, "USER=yas"
envp[1]==0xbffffa9c, "LOGNAME=yas"
envp[2]==0xbffffaa8, "HOME=/home/lab/Denjo/yas"
envp[3]==0xbffffac1, "PATH=/home/lab/Denjo/yas/bin:/bin:/usr/local/bin:/usr/local3/bin:/usr/java/jdk1.3.1_01/bin:/usr/bin:/usr/bin/X11:/usr/X11R6/bin:/usr/local/Acrobat4/bin:/usr/lib/ICAClient:/usr/pgi/linux86/bin"
<中略>
envp[44]==0xbfffff9c, "NAME=Yasushi Shinjo"
envp[45]==0xbfffffb0, "RHOST=bridge.hlla.is.tsukuba.ac.jp Wed"
envp[46]==0xbfffffd7, "SIGNATURE=Yasushi Shinjo"
% printenv | head -4 [←]
USER=yas
LOGNAME=yas
HOME=/home/lab/Denjo/yas
PATH=/home/lab/Denjo/yas/bin:/bin:/usr/local/bin:/usr/local3/bin:/usr/java/jdk1.3.1_01/bin:/usr/bin:/usr/bin/X11:/usr/X11R6/bin:/usr/local/Acrobat4/bin:/usr/lib/ICAClient:/usr/pgi/linux86/bin
% []
main の第3引数 envp と大域変数 environ は、同じ値である。環境変数には、 ホーム・ディレクトリの名前を保持している HOME、コマンドを検索するディ レクトリの名前のリストを表す PATH、標準的に使われるエディタを表す EDITOR などがある。

◆ライブラリ関数 getenv()

環境変数は、main() の引数や 外部変数 environ を見るよりも、ライブラリ 関数 getenv() を使ってアクセスした方が簡単である。 たとえば、ホーム・ディレクトリを表す環境変数 HOME を得るには、次のよう にすればよい。
char *homedir ;
     homedir = getenv("HOME");

◆環境変数とシェル変数

シェル変数と環境変数とは違う。 csh では、シェル変数は、set コマンドで設定し、環境変数はsetenv コマン ドで設定する。シェル変数は、そのシェルの中だけで参照できるが、環境変数 は、そのシェルから実行されるプロセスで参照できる。

csh の特殊な機能として、シェル変数 path (小文字)を変更すると、環境変数 PATH (大文字)が変化する。シェル変数と環境変数が連動しているものには、 他に、term, user, home などがある。

sh, bash では、シェル変数を export すると環境変数になる。

■プロセスの生成とプログラムの実行

UNIX では、新たにプロセスを作る時に、自分のコピーしか作れない(fork()シ ステム・コール)。元のプロセスを親プロセス、作られたプロセスを子プロセ スという。

現在のプログラムの実行はそのまま続けて、新しくプログラムを実行するには、 次のようにする。

execve() システム・コールの引数は、 main() の引数と関連している。

◆fork

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

実行結果


% cp ~yas/syspro/proc/fork-hello.c . [←]
% make fork-hello [←]
cc     fork-hello.c   -o fork-hello
% ./fork-hello  [←]
hello
hello
hello
hello
hello
hello
hello
hello
% []

fork() がなければ、1回しか表示されないはずである。1回の fork() でプ ロセスが2つに分かれる(新しいプロセスが1つ増える。)fork() した先の プロセスもさらに分かれるので、全部で2の3乗個のプロセスに分かれる。

◆execve()

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

実行結果


% cp ~yas/syspro/proc/exec-date.c . [←]
% make exec-date [←]
cc     exec-date.c   -o exec-date
% ./exec-date [←]
日  4月 27 23:49:30 JST 2003
% []

execve() は、最初の1回しか有効ではない。execve() に成功すると、現在の プログラムがメモリから消され、新しいプログラム(/bin/date)がメモリに読 込まれる。/bin/date は、仕事が終ると exit() する。元のプログラムにもど ることはない。

◆fork()、execve()、wait() の標準的な使い方

   1:	/*
   2:	        proc-create.c -- calプログラムよりプロセスを作る
   3:	        ~yas/syspro/proc/proc-create.c
   4:	        Start: 1995/02/27 15:27:54
   5:	*/
   6:	
   7:	#include <unistd.h>     /* fork(), execve(), pid_t */
   8:	#include <sys/types.h>  /* wait() */
   9:	#include <sys/wait.h>   /* wait() */
  10:	
  11:	extern char **environ;
  12:	
  13:	main()
  14:	{
  15:	   pid_t child_pid ;
  16:	   int status ;
  17:	        if( (child_pid=fork()) == 0 )
  18:	        {
  19:	            char *argv[4] ;
  20:	            printf("child: pid == %d, ppid == %d\n", getpid(), getppid() );
  21:	            argv[0] = "cal" ;
  22:	            argv[1] = "5" ;
  23:	            argv[2] = "2003" ;
  24:	            argv[3] = 0 ;
  25:	            printf("child: 3\n");sleep( 1 );
  26:	            printf("child: 2\n");sleep( 1 );
  27:	            printf("child: 1\n");sleep( 1 );
  28:	            printf("child: 0\n");
  29:	            execve( "/usr/bin/cal", argv, environ );
  30:	            perror( "execve" );
  31:	            /* exec に失敗したら exit() を忘れないこと */
  32:	            exit( 1 );
  33:	        }
  34:	        else if( child_pid > 0 )
  35:	        {
  36:	            printf("parent: pid == %d, ppid == %d, child_pid == %d\n",
  37:	                    getpid(), getppid(),child_pid );
  38:	            printf("parent: waiting ...\n");
  39:	            wait( &status );
  40:	            printf("status == %d\n",status );
  41:	        }
  42:	        else
  43:	        {
  44:	            perror("fork");
  45:	        }
  46:	}

実行例。

% cp ~yas/syspro/proc/proc-create.c . [←]
% make proc-create [←]
cc     proc-create.c   -o proc-create
% ./proc-create [←]
parent: pid == 21776, ppid == 21750, child_pid == 21777
parent: waiting ...
child: pid == 21777, ppid == 21776
child: 3
child: 2
child: 1
child: 0
      5月 2003
日 月 火 水 木 金 土 
             1  2  3
 4  5  6  7  8  9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31

status == 0
% []

fork() でプロセスが2つに分かれる。つまり、元のプロセスのコピーにより、 新しいプロセスが1つできる。コピーにより、fork() の結果は、違う。親プ ロセス(もとともあったプロセス)では、fork() の結果は、正の値が返され る。この正の値は、子プロセスの PID である。子プロセス(コピーの方)は、 0 が返される。

まれに、fork() が失敗することがある。その時には、子プロセスは作られず、 親プロセスに -1 が返される。

親プロセスと子プロセスは、独立に動作する。どちらが先に実行されるかとい う保証はない。本来は、独立に仕事をさせたい時に、プロセスを作るものであ り、1つのひとつ順序よくやりたい時にはプロセスを作らない。実行の順序を 制御するには、プロセス間で「同期」と呼ばれる操作を行う必要がある。

Unix の場合、別のプログラムをサブルーチンのように使いたいことがある。 この場合、子プロセスを fork() して、同期として、子供の終了(exit()する) を親が待つ(wait()する)。

それ以外の同期の方法は、少々複雑で、ファイルに対するロックやパイプなど のプロセス間通信を通じて行う。

sleep() は、引数で指定された秒数だけプロセスの実行を休止するライブラリ 関数である。

一度できたプロセスは、exit() しない限り、終了しない。main() がから抜け ると、プロセスが終了するのは、main() を呼び出した関数が exit() システ ムコールを実行しているからである。

start()
{
    ....
    ret = main(argc, argv, envp);
    exit( ret );
}
正常に終了した時には、exit(0)する。そうでない時には、0 以外の値で exit する。exit で返した値は、wait() で受け取ることができる。ただし、下位 8 ビットだけしか渡されない。WEXITSTATUS(status)で受け取る。

wait() で得られる status を調べると、exit() の値の他に、kill で殺され たかどうか、kill や ptrace() で停止したかどうかも調べられる。詳しくは、 man 2 wait を参照しなさい。

execve() は、システム・コールである。これを使いやすくするために、次の ようなライブラリ関数が用意されている。

     int execl (const char *path, const char *arg0, ..., const char *argn,
          (char *)0);
     int execv (const char *path, char *const *argv);
     int execle (const char *path, const char *arg0, ..., const char *argn,
          (char *0), const char *envp[]);
     int execve (const char *path, char *const *argv, char *const *envp);
     int execlp (const char *file, const char *arg0, ..., const char *argn,
          (char *)0);
     int execvp (const char *file, char *const *argv);
     int system(const char *string);
     FILE *popen(const char *command, const char *type);
     int pclose (FILE *stream);
親プロセスが子プロセスの終了を待ちたい場合には、wait() システムコール を使う必要がある。
pid_t wait(int *status)
pid_t waitpid(pid_t pid, int *status, int options);
pid_t wait3(int *status, int options,struct rusage *rusage)
pid_t wait4(pid_t pid, int *status, int options,struct rusage *rusage)
Linux の wait() には、必ず引数を与える。status が不要の時には、0 を与 える。

◆ライブラリ関数 system()

プログラムの中で他のプログラムをサブルーチンのようにして実行するには、 system() というライブラリ関数(システムコールではない)が便利である。
main()
{
    system("ls -l");
    system("wc <file.c");
}
引数の文字列では、/bin/sh の命令が記述可能である。

◆Javaでのプログラムの実行

java.lang.Runtime の exec() メソッドを使う。
   1:	/*
   2:	        RunProgram.java -- プログラムを実行するプログラム
   3:	        ~yas/syspro/proc/RunProgram.java
   4:	        Created on 2004/04/11 19:44:22
   5:	*/
   6:	
   7:	import java.lang.Runtime;
   8:	
   9:	public class RunProgram {
  10:	    public static void main( String not_used_argv[] )
  11:	    {
  12:	        try {
  13:	            Runtime rt = Runtime.getRuntime();
  14:	            String argv[] = new String[5];
  15:	            argv[0] = "kterm" ;
  16:	            argv[1] = "-e" ;
  17:	            argv[2] = "tr" ;
  18:	            argv[3] = "a-z" ;
  19:	            argv[4] = "A-Z" ;
  20:	            java.lang.Process child = rt.exec(argv);
  21:	            int status = child.waitFor();
  22:	            stdout.println("status == "+status);
  23:	        }
  24:	        catch( Exception e )
  25:	        {
  26:	            stderr.println( e );
  27:	        }
  28:	    }
  29:	    static java.io.BufferedReader stdin = 
  30:	        new java.io.BufferedReader( new java.io.InputStreamReader(System.in) );
  31:	    static java.io.PrintStream stdout = System.out;
  32:	    static java.io.PrintStream stderr = System.err;     
  33:	}

exec() の結果は、Process クラスのオブジェクトである。 実行終了を待ちたい時には、waitFor() を行う。 実行するプログラムの標準入力や標準出力を読み書きする機能もある。

■マニュアルの読み方

プログラムを作る時には、参考書だけでなく、付属のマニュアルを必ず参照す る必要がある。参考書とは微妙にコマンドや関数の使い方が違っていることが ある。(ただし、Linux に付属のマニュアルは、間違いを含んでいることもある。)

◆コマンドと同名のシステムコールのマニュアルを読む

Unix のマニュアルは、章ごとに分割されている。
1 章
コマンド
2 章
システム・コール
3 章
ライブラリ
4 章
デバイス・ファイル
5 章
ファイル形式
6 章
ゲーム
7 章
その他
8 章
管理者用のコマンド
単純に man write と打つと、write(1)、つまり、1章のwrite コマンドのマニュ アルが表示される。write(2) システム・コールを見るには、次のように打つ。
% man 2 write [←]

Unix System V (Solaris など) では、2章の write を読むには、次のように -s オプションを使う。

% man -s2 write [←]

各章の説明は、intro を読むとよい。たとえば、システムコールの場合は、次 のように打つ。

% man 2 intro [←]

■デバッガ

まだ使えない人は、練習すること。

◆gdb 5.0 日本語マニュアル

◆gdb コマンドカード(印刷用)

◆xxgdb

X Window のインタフェース。
% xxgdb progname & [←]
% []
xgdb ではなく、xxgdb。x が2回。

◆emacs ESC x gdb

Emacs (Mule) の中から gdb を呼ぶ。ソース・プログラム上でどこを実行して いるかを追うことができる。 使い方。
  1. emacs を実行する。
  2. ESC x gdb リターン」と打つ。
  3. gdb progname リターン」と打つ。
gdb のウィンドウでよく使うキー
C-c C-n
next
C-c C-s
step
C-c C-l
ウィンドウを割って、ソースの表示
ソース・プログラムのウィンドウで使えるキー
C-x SPC
ブレーク・ポイントの設定。(こちらは使える。)

◆Segmentaton fault(Segmentaton violation)

プログラムを実行して、Segmentaton fault というエラーが出たら、ポインタ の操作が怪しい。どこが怪しいかは、cc -g でコンパイルして、その中で実行 して、バックトレースすれば、だいたい分かる。

■練習問題

★練習問題 1 psコマンド

ps コマンドでプロセスを観察しなさい。

★練習問題 2 ccコマンドの観察

cc -v で、cc から実行されるプログラムを観察しなさい。

★練習問題 3 nmコマンドによる実行形式のシンボルテーブルの観察利用

nm で、オブジェクト・プログラムと実行形式のシンボルとそのアドレス を観察しなさい。

★練習問題 4 writeシステムコールによるライブラリの実現

次のライブラリ関数と似た動きをする(完全に同じでなくてもよい)関数を、 write() システムコールを使って実現しなさい。 writeの第一引数は、1 とする。 標準のライブラリ関数と区別するために、次のような名前にしなさい。 プログラムは、次のような形になる。
int myputchar(int c)
{
    ...
    write(1,...,...);
    ...
}

int myputs(char *s)
{
    ...
    write(1,...,...);
    ...
}
main() 関数を付けて、正しく動いていることがわかるようにしなさい。たと えば、次のようになる。

main()
{
	myputchar('h');
	myputchar('e');
	myputchar('l');
	myputchar('l');
	myputchar('o');
	myputchar('\n');
}

main()
{
	myputs("hello\n");
}
注意:myputchar() では、符合拡張にも気をつける。

myputchar() では、番地を調べて、その番地から1 バイトだけ write する。 myputs() では、文字列の長さを調べて、その長さ分のバイトだけ write する。

★練習問題 5 readシステムコールによるライブラリの実現

次のライブラリ関数のどれかを read() システムコールを使って実現しなさ い。 ただし、read()の第一引数は、0 とする。標準のライブラリ関数と区別するた めに、次のような名前にしなさい。 プログラムは、次のような形になる。
int mygetchar()
{
    ...
    read(0,...,...);
    ...
}
main() 関数を付けて、正しく動いていることがわかるようにしなさい。

注意:プログラムを実行する時に、stty cbreak; をした後に実行すると オペレーティング・システムによるバッファリングを抑止できる。

% cat [←]
hello[←]
hello
^D
% stty cbreak;cat [←]
hheelllloo[←]

^C
% []
通常の実行では、^D (Control D, Control キーを押しながら d キーを押す) で、プログラムに EOF (end of file) が伝わる。stty cbreak では、^D は効 かないので、^C でプログラムを強制終了させる。

できれば、myputchar() と共に解きなさい。

注意:ライブラリ関数 gets() には、バッファの長さ以上のデータが入力され た時に、問題がある。今後、決して使ってはいけない。

★練習問題 6 フィルタのプログラム

高水準入出力ライブラリ関数を持ちいて、次のようなフィルタを作りなさい。

文字単位のフィルタでは、getc(), fgetc(), getchar() と putc(), fputc(), putchar() などを使うとよい。行単位のフィルタでは、 fgets(), fputs(), fprintf() などを使うとよい。 (gets(), fscanf(), scanf() は、 バッファ・オーバーフローが起きるので、使 わない。)

fgrep は、grep, egrep などと違って単純な文字列の比較しかできない。 regcomp(3) などを使えば、正規表現の検索部分は自分で作らなくてもよい。

以下に rot-N 暗号プログラムの main() 関数の例を示す。

   1:	/*
   2:	        rotn.c -- 文字単位の簡単なフィルタ(rotN)
   3:	        ~yas/syspro/file/rotn.c
   4:	        Start: 2003/04/21 13:31:46
   5:	*/
   6:	
   7:	#include <stdio.h>      /* stdin, stdout, fopen(), fclose(), getc(), */
   8:	#include <ctype.h>      /* isalpha() */
   9:	#include <stdlib.h>     /* strtol() */
  10:	
  11:	main( int argc, char *argv[] )
  12:	{
  13:	    int n ;
  14:	    char *filename ;
  15:	        if( argc != 3 )
  16:	        {
  17:	            fprintf(stderr,"Usage: %% %s n file\n",argv[0]);
  18:	            exit( 1 );
  19:	        }
  20:	        n = strtol( argv[1],0,10 );
  21:	        filename = argv[2] ;
  22:	        printf("n==%d, file == %s \n", n, filename );
  23:	}
以下に fgrep プログラムの main() 関数の例を示す。
   1:	/*
   2:	        myfgrep.c -- 簡単な行単位のフィルタ(fgrep)
   3:	        ~yas/syspro/file/myfgrep.c
   4:	        Start: 2003/04/23 14:51:27
   5:	*/
   6:	
   7:	#include <stdio.h>      /* stdin, stdout, fopen(), fclose(), getc(), */
   8:	#include <string.h>     /* strstr() */
   9:	
  10:	main( int argc, char *argv[] )
  11:	{
  12:	    char *pattern ;
  13:	    char *filename ;
  14:	        if( argc != 3 )
  15:	        {
  16:	            fprintf(stderr,"Usage: %% %s pattern file\n",argv[0]);
  17:	            exit( 1 );
  18:	        }
  19:	        pattern = argv[1] ;
  20:	        filename = argv[2] ;
  21:	        printf("pattern==%s, file == %s \n", pattern, filename );
  22:	}

rot13 や rot-N などの文字単位のフィルタは、例題の filter-char.c の大部分をそのまま使い toupper() だけを置き換えても実現できる。

★練習問題 7 teeプログラム

file-copy.c, stdio-thru.c の2つのプログラムを参考にして、UNIX の tee プログラムと同じような機能をもつプログラムを作りなさい。(tee コマンド について詳しくは、man tee を見なさい。)プログラムの名前は、tee ではな く、別の名前(mytee) としなさい。tee の場合、標準の tee が動いているの か、自分で作った tee が動いているのか区別がつかない。ただし、出力する ファイルは、一つでもよい。-i オプションや -a オプションには対応しなく てもよい。

mytee は、次のようにして、標準入力を標準出力にコピーしながら、同時にファ イルにも保存するものである。

% grep pattern file1 | ./mytee result [←]
この結果として、画面には、次のように grep コマンドを実行した時と同じ結果が表示される。

% grep pattern file1 [←]

同時に、ファイル reult には、画面に出力された結果とまったく同じものが 保存される。

tee コマンドの名前は、アルファベットの「T」に由来する。図形的に考えれ ば、動きを理解することができる。tee コマンドは、左(パイプラインで標準 入力)から入ったデータを下(引数で与えられたファイル)に保存しながら、 右(標準出力)にも出力する。tee コマンドは、時間のかかる処理の結果や、 後で参照したい中間結果をファイルに保存するために利用される。次の例は、 makeコマンドの実行結果を make.out へ保存すると同時に、それを user にメー ルで送るものである。

% make |& tee make.out | mail -s "make result" user [←]

これにより、user は、メールが届いたことで、make コマンドの実行終了を知 ることができる。同時に、作業していたディレクトリでその結果を参照するこ とができる。

この課題では、入出力には、システムコール (open(),read(),write(),close()))を使いなさい。

★練習問題 8 catプログラム

UNIXの cat コマンドと同様の動きをするプログラムを作りなさい。このプロ グラムの名前を mycat とする。引数として、なにも与えられなかった時には、 標準入力を標準出力にコピーしなさい。また、引数として "-" が指定された 時には、その位置で標準入力を標準出力にコピーしなさい。たとえば、次のよ うな場合、file1 とfile2 の内容の間に、標準入力の内容をコピーしなさい。

% mycat file1 - file2 [←]

この課題では、入出力には、システムコール (open(),read(),write(),close()))を使いなさい。

★練習問題 9 echoコマンド

echo コマンドと似た動きをするプログラムをつくりなさい。

★練習問題 10 printenvコマンド

printenv コマンドと似た動きをするプログラムを作りなさい。

★練習問題 11 getenv()の利用

getenv() ライブラリ関数を利用して環境変数 HOME を取得し利用しなさい。 たとえば、~/.cshrc など、ホーム・ディレクトリにあるファイルを画面に表 示するプログラムを作りなさい。

★練習問題 12 putenv()の利用

putenv()、または、setenv() ライブラリ関数を利用して環境変数を変え、プ ログラムの動き(ライブラリ関数の動き)が変ることを確かめなさい。

★練習問題 13 getenv()の実現

getenv() ライブラリ関数と似た動きをする関数を作りなさい。

★練習問題 14 putenv()の利用

putenv() または、setenv() ライブラリ関数と似た動きをする関数を作りなさ い。既存の同じ名前の環境変数を上書きする時には、メモリが足りないことが ある点に注意する。単にその同じ番地に上書きすれば、次の環境変数が潰され る。

新しい環境変数を追加するには、environ の先の領域もコピーして増やす。

★練習問題 15 ライブラリ関数 system() の利用

ライブラリ関数 system() を使って、 proc-create.c を書き直しなさい。

★練習問題 16 ライブラリ関数 system()の内容

fork(), execve() (あるいは execl() など)、wait() などを使って、ライブ ラリ関数 system() と同じような関数を作りなさい。元々の system() と区別 するために、関数の名前は別のもの(たとえば、mysystem())としなさい。

シグナルについては何もしなくてもよい。

★練習問題 17 n回実行

指定された引数の回数だけプログラムを実行するようなプログラムをつくりな さい。たとえば、次の例では、date -u を3回実行している。
% ./run-n 3 date -u [←]
月  4月 28 03:01:11 UTC 2003
月  4月 28 03:01:11 UTC 2003
月  4月 28 03:01:12 UTC 2003
% []
argv[1] の文字列を、strtol() などで処理して数を得る。 その回数だけ、fork(),execve(),wait() をする。 &argv[2] 以降を、execve() にそのまま与えるとよい。

ライブラリ関数 system() も使ってもよいが、使わない方が簡単であろう。

main() プログラムの例を、~yas/syspro/proc/run-n.cに置く。

   1:	/*
   2:	        run-n.c -- 与えられたプログラム n 回実行するプログラム(mainのみ)
   3:	        ~yas/syspro/proc/run-n.c
   4:	        Start: 2003/04/27 23:57:38
   5:	*/
   6:	
   7:	#include <stdio.h>
   8:	
   9:	int run_n( int n, char *argv[] );
  10:	
  11:	main( int argc, char *argv[], char *envp[] )
  12:	{
  13:	    int n ;
  14:	        if( argc < 2 )
  15:	        {
  16:	            fprintf(stderr,"Usage: %% %s num cmd arg1 arg2 ...\n",argv[0] );
  17:	            exit( -1 );
  18:	        }
  19:	        n = strtol( argv[1],0,10 );
  20:	        run_n( n, &argv[2] );
  21:	}
  22:	
  23:	int run_n( int n, char *argv[] )
  24:	{
  25:	    int i ;
  26:	        printf("n == %d \n",n );
  27:	        for( i=0 ; argv[i] ; i++ )
  28:	        {
  29:	            printf("argv[%d]: %s\n",i,argv[i] );
  30:	        }
  31:	}
  32:	

★練習問題 18 カウントダウン

次のようにカウントダウンするプログラムを、execve() (あるいは execl() など)を使って作りなさい。
% ./countdown 3 [←]
3
2
1
0
% []
このプログラムでは、単に表示がこのようになるのではなくて、カウント・ダ ウンするたびに必ず execve() などでプログラムを切替えなさい。次のような プログラムは、「不可」である。

main(int argc, char *argv[])
{
    int n, i ;
    n = strtol( argv[1],0,10 );
    for( i=n ; i>=0 ; i-- )
    {
        printf("%d\n",i);
    }
}

カウント・ダウンするたびに、execve() などでプログラムを切替えること。

main(int argc, char *argv[])
{
    ...
    n = strtol( argv[1],0,10 );
    if( n <= 0 )
       exit( 0 );
    else
    {
	n -- ;
	...
	execve(....);
	perror("execve");
	exit( 1 );
    }
}

注意:このプログラムでは、fork() も wait() も、不要である。

★練習問題 19 if-then-else

プログラムを実行し、その結果に応じて別のプログラムを実行するようなプロ グラムを作りなさい。たとえば、次のように実行する。
% ./if-then-else "cc file1.c" "play happy" "play sad" [←]
argv[1] で与えられたプログラムを実行し、成功すれば、argv[2] を、実行す る。失敗すれば、argv[3] を実行する。実行する時には、上のように 引用符でコマンドを括ると、間に空白を含まれていても1つの引数になる。 このような引数は、ライブラリ関数 system() に渡す時に都合がよい。

コマンドの区切りは、別のもでもよい。

% ./if-then-else cc file1.c then play happy else play sad [←]
この場合、argv[3] の then や argv[6] の else を 0 で潰して execvp() な どで実行するとよい。この場合は、system() よりも、execvp() を使った方が 簡単であろう。まず最初の条件判定をする部分を fork-exec で実行し、wait する。wait の結果、成功していたら、then 以下の部分をexec し、そうでな ければ、else 以下の部分を exec する。then 以下や else 以下を実行する時 には、もどってくる必要がないので、fork しなくてもよい。

★練習問題 20 並列wget

wget コマンドは、引数で与えられた URL の WWW 資源をローカル・ファイル に落とすプログラムである。
% wget http://www/ [←]
--02:00:20--  http://www/
           => index.html
www:80 に接続しています... 接続しました!
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 2,443 [text/html]

    0K -> ..                                                     [100%]

02:00:20 (2.33 MB/s) - index.html を保存しました [2443/2443]

% []
複数の wget コマンドを同時に実行して、複数の WWW 資源を同時にコピーす るプログラムをつくりなさい。たとえば、次のように実行すると、最大3個の wget コマンドを同時に動かし、同時にコピーを行う。
% ./run-wget-n 3 url1 url2 url3 url4 url5 url6 [←]
このプログラムでは、ライブラリ関数 system() を使ってもよい(使わなくて もよい)。

必ずしも同数に分割する必要はない。高度なプログラムを記述すれば、終了し 次第、次の URL に進むようにすることができる。wget コマンドにはもともと も複数の URL を取る機能もあるので、それを使ってもよい。

★練習問題 21 manの表示

man コマンド、xman 、Emacs の ESC x man の使い方を練習しなさい。

man で表示された #includeの部分や定数を、Xウインドウ、または、mule の コピー&ペースト機能を使ってソース・プログラムに取り込みなさい。

★練習問題 22 デバッガ

cc -g で作ったプログラムを、gdb, xxgdb, Emacs gdb を使って実行しなさい。 デバッガのコマンド実行してみなさい。

★練習問題 23 Segmentation fault

Segmentation fault を起こすプログラムをかきなさい。 それをデバッガで追跡して、どこで起きたかを調べてなさい。
Last updated: 2004/05/17 01:44:13
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>