プロセス間通信(3)/UDP/IPとシグナル

システム・プログラム

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

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

■今日の重要な話

UDP/IP のプログラム ソフトウェア割込みとシグナル

■TCP/IPの基本的な考え方

◆通信路の開設

■echo-client

■echo-server-fork

■補足

◆emacs indent-region

字下げ(indentation)を変更する時には、1行1行タブを打つのではなくて、 M-x indent-region を使うとよい。

◆echo-server-pthread.c

fork() する変りにスレッド(POSIX Thread, PThread)を作る方法がある。 fork() よりも、プログラムはすっきりしている。 ~yas/syspro/ipc/echo-server-pthread.c スレッドの話は、3学期の授業「オペレーティングシステムII」などで出てく る。

■emacs indent-region

■UDP/IPの考え方

UDP/IP は、IP と同じく、データグラム転送サービスを提供する。データグラ ムでは、ストリーム(TCP/IP)とは異なり、通信路を開設することなく、アド レスを指定してデータを送る。UDP/IP では、アドレスとしては、次の2つを 使う。
  1. IP アドレス
  2. ポート番号

IP 層では、アドレスとしては、IP アドレスだけを使う。UDP/IP は、IP 層の 機能をほとんどそのままの形で利用可能にしたものである。違いは、ポート番 号を指定できることと、チェックサムでデータの内容を検査できることなどで ある。

UDP/IP でも、クライアント側のアドレスは、OSに任せ、自らは指 定しないことが多い(指定することもできる)。

図? UDP/IPのデータグラム

図? UDP/IPのデータグラム

◆UDPの特徴

TCP/IP で提供されているストリームと比較して、UDP/IP で提供されているデー タグラムには次のような特徴がある。 UDP/IP では、connect() しなければ、1つのソケットで、複数の通信相手と データを送受信することができる。通信相手は、sendto() で指定し、 recvfrom() で任意の場所から受信することが普通である。connect() システ ムコールを使うと、特定の相手としか通信できなくなるが、connect() を使う 方法は、UDP/IP のプログラムの書き方としては一般的ではない。

◆telnet

telnet は、TCP/IP の汎用のクライアント(文字列の送受信用)なので、UDP/IP でサービスを提供するクライアントとしては使えない。

■echo-client-udp

UDP/IPのクライアント側のプログラムで大事な標準のシステムコールとライブ ラリ関数は、次の通りである。

UDP/IP のポート番号 7 (echo) では、受け取ったデータをそのまま返すサー ビスを提供している。ただし、セキュリティ上の理由から、echo サービスを 停止することが、一般的である。以下は、このサービスを利用するクライアン トである。


----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        echo-client-udp.c -- 文字列を送受信するクライアント(UDP/IP版)
   4:	        ~yas/syspro/ipc/echo-client-udp.c
   5:	        Start: 1997/06/16 21:22:26
   6:	*/
   7:	#include <stdio.h>
   8:	#include <sys/types.h>  /* socket() */
   9:	#include <sys/socket.h> /* socket() */
  10:	#include <netinet/in.h> /* struct sockaddr_in */
  11:	#include <netdb.h>      /* getaddrinfo() */
  12:	
  13:	extern void echo_client_udp( char *server, int portno );
  14:	extern int udp_port_nobind();
  15:	extern int sockaddr_in_init( struct sockaddr_in *addr, int addrlen,
  16:	                             char *hostname, int portno );
  17:	extern void sockaddr_print( struct sockaddr *addrp, int addr_len );
  18:	extern void sockname_print( int s );
  19:	
  20:	main( int argc, char *argv[] )
  21:	{
  22:	     char *server ;
  23:	     int portno ;
  24:	        if( argc != 3 )
  25:	        {
  26:	            fprintf( stdout,"Usage: %% %s host port\n",argv[0] );
  27:	            exit( -1 );
  28:	        }
  29:	        server = argv[1] ;
  30:	        portno = strtol( argv[2],0,10 );
  31:	        echo_client_udp( server, portno );
  32:	}
----------------------------------------------------------------------

main の部分は、TCP/IP 版の echo-client.c とほとんど同じである。

main() 関数は、コマンドラインの引数を調べて、echo_client() を読んでい る。strtol() で、文字列で与えられたポート番号を、int に変換している。


----------------------------------------------------------------------
  33:	
  34:	#define BUFFERSIZE      1024
  35:	
  36:	void
  37:	echo_client_udp( char *server, int portno )
  38:	{
  39:	    int          sock ;
  40:	    int          slen,scount,rcount ;
  41:	    char         sbuf[BUFFERSIZE];
  42:	    char         rbuf[BUFFERSIZE];
  43:	    int          i ;
  44:	    int          fromlen ;
  45:	    struct sockaddr_in to; struct sockaddr_storage from ;
  46:	
  47:	        sock = udp_port_nobind();
  48:	        if( sock<0 )
  49:	            exit( -1 );
  50:	    printf("my port is "); sockname_print( sock ); printf("\n");
  51:	
  52:	        if( sockaddr_in_init( &to, sizeof(to), server, portno )<0 )
  53:	        {
  54:	            perror("sockaddr_in_init");
  55:	            exit( -1 );
  56:	        }
  57:	    printf("server is ");
  58:	    sockaddr_print( (struct sockaddr *)&to, sizeof(to) ); printf("\n");
  59:	
  60:	        printf("==> "); fflush(stdout);
  61:	        while( fgets(sbuf,BUFFERSIZE,stdin) )
  62:	        {
  63:	            slen = strlen( sbuf );
  64:	
  65:	    printf("sending [%s] (%d bytes) to ", sbuf, slen );
  66:	    sockaddr_print( (struct sockaddr *)&to, sizeof(to) ); printf("\n");
  67:	
  68:	            scount = sendto( sock, sbuf, slen, 0,
  69:	                             (struct sockaddr *)&to, sizeof(to) );
  70:	            if( scount != slen )
  71:	            {
  72:	                perror("sendto()");
  73:	                exit( 1 );
  74:	            }
  75:	    printf("after sendo(), my port is "); sockname_print( sock ); printf("\n");
  76:	
  77:	            fromlen = sizeof( from );
  78:	            rcount = recvfrom( sock, rbuf, BUFFERSIZE, 0,
  79:	                               (struct sockaddr *)&from, &fromlen);
  80:	            if( rcount < 0 )
  81:	            {
  82:	                perror("recvfrom()");
  83:	                exit( 1 );
  84:	            }
  85:	            rbuf[rcount] = 0 ;
  86:	    printf("received %d bytes [%s] from ",rcount, rbuf );
  87:	    sockaddr_print( (struct sockaddr *)&from, fromlen ); printf("\n");
  88:	
  89:	            printf("==> "); fflush(stdout);
  90:	        }
  91:	        printf("\n");
  92:	
  93:	        close( sock );
  94:	}
----------------------------------------------------------------------

echo_client_udp() では、udp_port_nobind() という関数を呼び出している。 この結果、UDP/IP のポートが作られ、通信可能なファイル記述子が返される。 ただし、サーバとの間には、通信路は確保されない。このファイル記述子は、 標準入出力(0,1,2)や open() システム・コールの結果と同じもので、不要に なったら close() で解放する。しかし、この状態では、write() や read() システムコールは使えない。その代りに、sendto() や recvfrom() システム コールを使う。

クライアント側では、bind() でアドレス(IPアドレスとポート番号)を設定し ないことが多い。通信に使う前には、アドレスは固定されていない。通信に使 うと、その瞬間に固定され、close() するまで有効である。(クライアント側 でも、bind() でアドレスを固定することもできるが、一般的ではない。)

sockaddr_in_init() は、構造体 to に送り先のアドレス(IPアドレスとポート 番号)をセットしている。

送るデータは、TCP/IP版の同じく文字列である。UDP/IP の通信では、データ の区切りは保存されるので、行末の区切りをつけずに構造体をそのまま送るも のも多い。

sendto() システムコールで、データを送信している。送り先は、第5引数に指 定されている。TCP/IP とは異なり、データの区切りは保存される。また、一 度に全データが送られるので、TCP/IP とは異なり、writen() のように、全デー タを送り出すためのループは不要である。ただし、一度に送ることができるデー タの大きさには上限がある。

途中、階層の通信媒体の上限を越えた場合、「フラグメント化」が行われ、実 際のデータとしては分割されることがある。たとえば、イーサネットでは、約 1500バイトを越えた場合、フラグメント化される。フラグメント化されたデー タは、受信側のプロセスに送られる前に組み立てられる。

データを受け取るには、recvfrom() システムコールを使っている。この時、 データの送り手のアドレス(IPアドレスとポート番号)が&from番 地に保存される。普通は、sendto() と同じアドレスから返ってくるが、 違うアドレスから返ってくることもある。

recvfrom() では、最後に終端の 0 を付けてくれないので、自分で 0 を付け ている。


----------------------------------------------------------------------
  95:	
  96:	int
  97:	udp_port_nobind()
  98:	{
  99:	    int s ;
 100:	        if( (s = socket(PF_INET, SOCK_DGRAM, 0)) < 0 )
 101:	        {
 102:	            perror("socket");
 103:	            return( -1 );
 104:	        }
 105:	        return( s );
 106:	}
----------------------------------------------------------------------

udp_port_nobind() は、クライアント側の UDP/IP のポートを作る関数である。 内部では、ソケットを socket() システムコールで作成しているだけである。 PF_INET と SOCK_DGRAMの組み合わせなので、 UDP を使うことを意味する。 socket() の引数で、PF_INET の変りに、AF_INET と書いてもよい。ここでは、 Protocol を選んでいるので、PF_ が正しいが、実際には、PF_INET と AF_INET は同じであり、また、多くのテキストで混在されて使われいる。

UDP/IP でも、connect() システムコールで接続先を固定する方法もあるが、 一般的ではない。接続先を固定した場合は、sendto() や recvfrom() ではな く、write() やread() を使って通信することもできる。その場合も、UDP の 性質は保たれるので、write() したデータのパケットが失われた時には、再転 送は行われない。


----------------------------------------------------------------------
 107:	
 108:	int
 109:	sockaddr_in_init( struct sockaddr_in *addr, int addrlen,
 110:	                  char *hostname, int portno )
 111:	{
 112:	    struct addrinfo hints, *ai;
 113:	    int err ;
 114:	
 115:	        if( addrlen < sizeof(struct sockaddr_in) )
 116:	        {
 117:	            fprintf(stderr,"sockaddr_in, not enough space (%d) > (%d)\n",
 118:	                     addrlen, sizeof(struct sockaddr_in) );
 119:	            return( -1 );
 120:	        }
 121:	        memset( &hints, 0, sizeof(hints) );
 122:	        hints.ai_family   = AF_INET ;
 123:	        hints.ai_socktype = SOCK_DGRAM ;
 124:	        if( (err = getaddrinfo( hostname, NULL, &hints, &ai )) )
 125:	        {
 126:	            fprintf(stderr,"unknown host %s (%s)\n",hostname,
 127:	                    gai_strerror(err) );
 128:	            return( -1 );
 129:	        }
 130:	        if( ai->ai_addrlen > addrlen )
 131:	        {
 132:	            fprintf(stderr,"sockaddr too large (%d) > (%d)\n",
 133:	                    ai->ai_addrlen,sizeof(addr) );
 134:	            freeaddrinfo( ai );
 135:	            return( -1 );
 136:	        }
 137:	        memcpy( addr, ai->ai_addr, ai->ai_addrlen );
 138:	        addr->sin_port = htons( portno );
 139:	        freeaddrinfo( ai );
 140:	
 141:	        return( 0 );
 142:	}
----------------------------------------------------------------------

UDP/IP の場合も、TCP/IP と同様に、アドレスの指定には、sockaddr_in 構造 体を使う。sendto() システムコールのマニュアルには、sockaddr 構造体を使 うようにと書かれているが、UDP/IP (IPv4) では、そのサブクラス(オブジェ クト指向用語)であるsockaddr_in を使う。この場合、IP アドレスとポート番 号が埋められている必要がある。その他に先頭に、sockaddr_in であることを 示す定数 AF_INET を置く。 getaddrinfo() は、ホスト名を IP アドレスし、さらに、先頭に定数 AF_INET を設定している。ポート番号は、第2引数が NULL なので、0 を指定している。 この例では、引数で与えられたポート番号を htons() を介してバイトオーダ を調整してから設定している。

getaddrinfo() に関しては、 TCP/IP 版の echo-client.c も、参照しなさい。


----------------------------------------------------------------------
 143:	
 144:	void
 145:	sockaddr_print( struct sockaddr *addrp, int addr_len )
 146:	{
 147:	    char host[BUFFERSIZE] ;
 148:	    char port[BUFFERSIZE] ;
 149:	        if( getnameinfo(addrp, addr_len, host, sizeof(host),
 150:	                        port, sizeof(port), NI_NUMERICHOST|NI_NUMERICSERV)<0 )
 151:	            return;
 152:	        printf("%s:%s", host, port );
 153:	}
 154:	
 155:	void
 156:	sockname_print( int s )
 157:	{
 158:	    struct sockaddr_storage addr ;
 159:	    int len ;
 160:	        len = sizeof( addr );
 161:	        if( getsockname( s, (struct sockaddr *)&addr, &len )< 0 )
 162:	        {
 163:	            perror("getsockname");
 164:	            exit( -1 );
 165:	        }
 166:	        sockaddr_print( (struct sockaddr *)&addr,len );
 167:	}
----------------------------------------------------------------------

sockaddr_print() は、sockaddr 構造体を表示する関数である。 getaddrinfo() ライブラリ関数により、ホスト名とポート番号の文字列表現を 得ている。文字列といっても、NI_NUMERICHOST|NI_NUMERICSERV というフラグ を指定しているので、結果は "123.4.5.6" や "80" のような数字の並びによ る表記になる。

sockname_print() は、自分自身のポートのアドレスを表示する関数である。 アドレスは、getsockname() で得られる。なお、UDP/IP では、TCP/IP とは異 なり、getpeername() で通信相手を調べることは、この場合できない。UDP/IP では、通信相手は、recvfrom() の引数で知ることができる。

実行例:


----------------------------------------------------------------------
% cp ~yas/syspro/ipc/echo-client-udp.c . [←]
% make echo-client-udp [←]
cc     echo-client-udp.c   -o echo-client-udp
% ./echo-client-udp localhost 7 [←]
my port is 0.0.0.0:0
server is 127.0.0.1:7
==> hello[←]
sending [hello
] (6 bytes) to 127.0.0.1:7
after sendo(), my port is 0.0.0.0:33041
received 6 bytes [hello
] from 127.0.0.1:7
==> world![←]
sending [world!
] (7 bytes) to 127.0.0.1:7
after sendo(), my port is 0.0.0.0:33041
received 7 bytes [world!
] from 127.0.0.1:7
==> ^D
% ./echo-client-udp adonis8 7 [←]
my port is 0.0.0.0:0
server is 130.158.86.28:7
==> aaa[←]
sending [aaa
] (4 bytes) to 130.158.86.28:7
after sendo(), my port is 0.0.0.0:33041
received 4 bytes [aaa
] from 130.158.86.28:7
==> bbb[←]
sending [bbb
] (4 bytes) to 130.158.86.28:7
after sendo(), my port is 0.0.0.0:33041
received 4 bytes [bbb
] from 130.158.86.28:7
==> ^D
% []
----------------------------------------------------------------------
このプログラムは、コマンドラインから2の引数をとる。第1引数は、ホスト 名、第2引数は、ポート番号である。このプログラムは、標準入力から得られ た行を、その指定されたホスト上のポート番号で動作しているサーバに対して 送る。サーバは、 同じものを送り返してくるので、それを受け取る。

■echo-server-udp.c

UDP/IP のポート番号 7 (echo) では、受け取ったデータをそのまま返すサー ビスを提供している。以下は、これと同じような機能を提供するサーバである。

----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        echo-server-udp.c -- 文字列を送受信するサーバ(UDP/IP版) 
   4:	        ~yas/syspro/ipc/echo-server-udp.c
   5:	        Start: 1997/06/16 21:22:26
   6:	*/
   7:	#include <stdio.h>
   8:	#include <sys/types.h>  /* socket() */
   9:	#include <sys/socket.h> /* socket() */
  10:	#include <netinet/in.h> /* struct sockaddr_in */
  11:	#include <netdb.h>      /* getaddrinfo() */
  12:	
  13:	extern void echo_server_udp( int portno );
  14:	extern int udp_port_bind( int portno );
  15:	extern void sockaddr_print( struct sockaddr *addrp, int addr_len );
  16:	extern void sockname_print( int s );
  17:	
  18:	main( int argc, char *argv[] )
  19:	{
  20:	    int portno ;
  21:	        if( argc >= 3 )
  22:	        {
  23:	            fprintf( stdout,"Usage: %s [portno]\n",argv[0] );
  24:	            exit( -1 );
  25:	        }
  26:	        if( argc == 2 )
  27:	            portno = strtol( argv[1],0,10 );
  28:	        else
  29:	            portno = getuid();
  30:	        echo_server_udp( portno );
  31:	}
  32:	
----------------------------------------------------------------------

TCP/IP版echo-server-fork と同様に、 引数として、ポート番号を取る。ポート番号が与えられなければ、そのプロセ スの UID (User ID) から生成する。UID は、個人個人を識別するための番号 (16ビット程度、システムによっては 32 ビット)である。この課題では、(1台 のコンピュータでは)個人ごとに別々の UID を使う必要がある。上のように UID から生成する方法は、一般的ではない。(筑波大学情報学類のシステムで はうまく働く。)


----------------------------------------------------------------------
  33:	#define BUFFERSIZE      1024
  34:	
  35:	void echo_server_udp( int portno )
  36:	{
  37:	    int s, rcount, scount, addrlen ;
  38:	    struct sockaddr_storage addr ;
  39:	    char buffer[BUFFERSIZE];
  40:	
  41:	        s = udp_port_bind( portno );
  42:	        if( s<0 )
  43:	            exit( -1 );
  44:	    printf("my port is "); sockname_print( s ); printf("\n");
  45:	
  46:	        while( 1 )
  47:	        {
  48:	            addrlen = sizeof( addr );
  49:	            rcount = recvfrom( s, buffer, BUFFERSIZE, 0,
  50:	                               (struct sockaddr *)&addr, &addrlen );
  51:	            if( rcount < 0 )
  52:	            {
  53:	                perror("recvfrom()");
  54:	                exit( 1 );
  55:	            }
  56:	            buffer[rcount] = 0 ;
  57:	    printf("received %d bytes [%s] from ",rcount, buffer );
  58:	    sockaddr_print( (struct sockaddr *)&addr,addrlen ); printf("\n");
  59:	
  60:	    printf("sending back [%s] (%d bytes) to ", buffer, rcount );
  61:	    sockaddr_print( (struct sockaddr *)&addr,addrlen ); printf("\n");
  62:	            scount=sendto( s, buffer, rcount, 0, (struct sockaddr *)&addr, addrlen );
  63:	            if( scount!= rcount )
  64:	            {
  65:	                perror("sendto()");
  66:	                exit( 1 );
  67:	            }
  68:	        }
  69:	}
  70:	
----------------------------------------------------------------------

udp_port_bind() は、引数で与えられたポート番号を使ってUDP/IP のポート を作成し、そのファイル記述子(ソケット)を返す。sockname_print() は、そ のアドレス(IPアドレスとポート番号)を表示する。 TCP/IP とは異なり、このポートは、そのまま通信に使うことができる。 逆に、accept() は使うことはできない。

サーバのプログラムの特徴は、内部に無限ループを持っていることである。 サーバは、普通の状態では、終了しない。

このサーバは、fork() しない。サービスの内容が簡単であり、sendto() シス テムコールでブロックすることもないからである。サービスが重たい時には、 他のクライアントからの処理を並列に進めるために、子プロセスを作ること可 能である。


----------------------------------------------------------------------
  71:	int udp_port_bind( int portno )
  72:	{
  73:	    struct sockaddr_in addr ;
  74:	    int addr_len ;
  75:	    int s ;
  76:	
  77:	        if( (s = socket(PF_INET, SOCK_DGRAM, 0)) < 0 )
  78:	        {
  79:	            perror("socket");
  80:	            return( -1 );
  81:	        }
  82:	
  83:	        memset( &addr, 0, sizeof(addr) );
  84:	        addr.sin_family = AF_INET ;
  85:	        addr.sin_addr.s_addr = INADDR_ANY ;
  86:	        addr.sin_port = htons( portno );
  87:	
  88:	        if( bind(s,(struct sockaddr *)&addr,sizeof(addr)) < 0 )
  89:	        {
  90:	            perror("bind");
  91:	            fprintf(stderr,"port number %d is already used. wait a moment or kill another program.\n", portno );
  92:	            return( -1 );
  93:	        }
  94:	        return( s );
  95:	}
  96:	
  97:	
<以下省略>
  99:	sockaddr_print( struct sockaddr *addrp, int addr_len )
...
 110:	sockname_print( int s )
..
----------------------------------------------------------------------

udp_port_bind() は、サーバ用に、ポート番号を固定した UDP/IP のポートを作成する。 クライアント側との違いは、 bind() システムコールを使っている点にある。

bind() システムコールの引数は、 TCP版のecho-server-fork と同じである。 ソケットが作成できたら、bind() システムコールで、サーバ側で利用するア ドレス(IPアドレスとポート番号)を設定する。IP アドレスは、普通は、 INADDR_ANY を指定する。複数の IP アドレスがある時には、どれに要求が来 ても受け付ける。特定の IP アドレスを指定すると、そのアドレスに来た要求 だけを受け付けるようになる。

ポート番号は、引数で与えられたものを、htons() でネットワーク・バイトオー ダに変換して与える。

listen() システムコールは、UDP/IP では使われない。 要求、すなわち、データが大量に送られ、サーバが処理できない時には、(普 通は古いものから順に)捨てられる。

実行例。

サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。

注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。


----------------------------------------------------------------------
% ./echo-server-udp 1231 [←]
my port is 0.0.0.0:1231
received 6 bytes [hello
] from 127.0.0.1:33042
sending back [hello
] (6 bytes) to 127.0.0.1:33042
received 7 bytes [world!
] from 127.0.0.1:33042
sending back [world!
] (7 bytes) to 127.0.0.1:33042
received 4 bytes [aaa
] from 130.158.86.28:33015
sending back [aaa
] (4 bytes) to 130.158.86.28:33015
received 4 bytes [133
] from 130.158.86.28:33015
sending back [133
] (4 bytes) to 130.158.86.28:33015
^C
% []
----------------------------------------------------------------------
サーバ側では、bind() をつかっているので、最初からポート番号が固定され ていることがわかる。

■入出力装置(デバイス)

デバイス(device) とは、コンピュータの箱の中に内蔵されているハードウェアの部品やケーブル で外で接続する部品。 普通は、CPU とメモリ以外のことを指す。 周辺装置(peripheral device)、 入出力装置(IO device)とも呼ばれる。

◆ハードウェアの構成

メモリ、CPU、デバイスは、バス(bus)(システム・バス(system bus))を通 じて接続されている。

図1 バスにより接続されたCPU、メモリ、デバイス

図1 バスにより接続されたCPU、メモリ、デバイス

バス:何本かの配線の束

アドレスバス
メモリのアドレスを示すための線
データバス
データを送るための線
コントロールバス
その他、制御用の線

◆デバイス・コントローラ

各デバイスとCPU で実行されるプログラムとデバイスとり橋渡しをする機器。

例:キーボード用のコントローラの働き

データが、電気信号などの形で送られてくる。コントローラの中のレジスタ (小容量のメモリ)に保存され。

◆CPUとデバイスの間のデータの受け渡しの方法

CPU から見える場所

  1. デバイス・コントローラが I/O 空間にあり、ポート番号と呼ばれる番地 が振られている。CPU が、ポート番号を指定して入出力命令を実行すると、 I/O 空間にあるレジスタの内容が読み書きできる(I/O 空間を示す信号線が1に なる)。
  2. デバイス・コントローラが、普通のメモリと同じ空間にあり、番地が番 地が振られている。CPU が、その番地へ普通のロード命令やストア命令でアク セスすると、コントローラのレジスタの内容が読み書きできる。
入出力の時に CPU が働くかどうか
  1. CPU が入出力命令、あるいは、ロード命令/ストア命令を実行する。
  2. DMA (Direct Memory Access)やバスマスタと呼ばれる機器があり、その 機器がCPU が普通に命令を実行している合間をぬって一時的にバスを乗っ取り、 データをメモリにコピーする。

◆ポーリングと割込み

CPU の速度に比べて、デバイスの速度は遅い。

ポーリング(polling): 周期的にデバイス・コントローラの状態をチェックする。 割込み(interrupt):入出力が可能になった時にデバイスがCPUに知らせる。
入力デバイス
コントローラは、入力データが到着すると、制御バスの割込み要求(IRQ, Interrupt Request)信号線を1にする。 (DMAやバスマスタを使っている時には、メモリへのコピーが完了した時)
出力デバイス
コントローラは、出力用バッファが空になると、割込み要求信号線を1に する。
CPU は、割込み要求を受け付けると、現在実行中の処理を中断して、 割込み処理ルーチン あるいは 割込みハンドラ と呼ばれるプログラムを実行する。 割込み処理ルーチンでは、実際に入力命令を実行したり、 次のデータを出力を開始する。 最後に、割込み処理から復帰する命令を実行する。すると、先ほど中断してい た処理が再開される。

■ソフトウェア割込みとシグナル

普通の割込み(ハードウェアによる割込み)は、オペレーティング・システム のカーネルにおいて利用されている。

ソフトウェア割込みとは、本来はオペレーティング・システムのカーネルしか 使えない割込みの機能を、ソフトウェアにより実現して、一般の利用者プログ ラム(プロセス)でも使えるようにしたものである。

UNIXでは、ソフトウェア割込みの機能は、シグナル(signal)という名前で実現 されている。シグナルとは、本来はプロセス間通信の一種で、あるイベントが起き たことを他のプロセスに知らせることである。ここで伝わるのは、あるイベントが 起きたかどうかだけで、イベントの種類の区別ができることがあるが、 データを送ることはできない。

UNIXでは、プロセ ス間でシグナルにより通信をする他に、キーボードからシグナルを送ることも できる。これは、「ソフトウェア割込み」として、プロセス1つひとつに割込 みボタンが付いているようなものである。また、プログラムの中で例外 (exception)が起きた時にも、ハードウェアの割込みと同様に、ソフトウェ ア割込みが生じる。これも、他のシグナルと同じように受け取ることができる。

UNIXのソフトウェア割込み(シグナル)を使うには、次のようなことが必要で ある。

割り込みハンドラが設定されていない時には、プロセスは、デフォルトの動き をする。デフォルトには、次の3つがある。 signal の種類は、man 7 signal (Linux) で表示される。

ソフトウェア割り込みは、1つのプログラムの中に制御の流れが1つしかない ようなプログラムの時に有効な方法である。最近のマルチスレッドのプログラ ムでは、シグナルの意味が不明確である。

スレッドの話は、3学期の授業「オペレーティングシステムII」などで出てく る。

◆signal-int.c


----------------------------------------------------------------------
   1:	/*
   2:	        signal-int.c -- SIGINT を3回受け付けて終了するプログラム。
   3:	        ~yas/syspro/proc/signal-int.c
   4:	        Start: 1997/05/26 18:38:38
   5:	*/
   6:	
   7:	#include <stdio.h>
   8:	#include <signal.h>
   9:	
  10:	int sigint_count = 3 ;
  11:	void sigint_handler();
  12:	
  13:	main()
  14:	{
  15:	        signal( SIGINT, &sigint_handler );
  16:	        printf("main(): going into infinite loop, sigint_count == %d\n", 
  17:	                sigint_count);
  18:	        while( 1 )
  19:	        {
  20:	            printf("main(): sigint_count == %d, pause() ....\n",
  21:	                    sigint_count );
  22:	            pause();
  23:	            printf("main(): return from pause().  sigint_count == %d\n",
  24:	                    sigint_count );
  25:	        }
  26:	}
  27:	
  28:	void sigint_handler()
  29:	{
  30:	        printf("sigint_handler():\n");
  31:	        if( -- sigint_count <= 0 )
  32:	        {
  33:	            printf("sigint_handler(): exit() ... \n");
  34:	            exit( 1 );
  35:	        }
  36:	/*      signal( SIGINT, &sigint_handler ); /* System V */
  37:	        printf("sigint_handler(): sigint_count == %d\n",sigint_count);
  38:	}
----------------------------------------------------------------------

signal() で、割込み処理ハンドラを登録している。この段階で、関数 sigint_handler() は呼び出されない。また、プログラムのどこからも、関数 sigint_handler() は明示的には呼び出されていない。

main() は、無限ループになっている。

pause() は、割込みを待つシステムコールである。システムコールの実行中に 割込みが発生するまで待つ。発生すると、割込み処理ハンドラの実行後にリター ンする。

sigint_handler() は、main() から呼び出されないが、割込みが発生すると実 行される。内部では、大域変数 sigint_count を減らして、0 以下になれば終 了している。

System V 系の Unix では、一度割込みハンドラが呼び出されると、それが解 除される。もう一度同じ割込みハンドラを使いたい場合には、割込みハンドラ の中で signal() で登録し直す必要がある。BSD 系の Unix と、 新しい目の Linux (glibc2 以降)では、再登録の必要はない。

古い Linux (libc4,libc5) では、標準では、System V と同じ動きをしていた。 Linux (RedHat 7.1)の日本語のマニュアルは、その時から更新されていない。 英語のマニュアルは、更新されている。

実行例。


----------------------------------------------------------------------
% stty -a [←]
speed 9600 baud; rows 40; columns 80; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = ;
eol2 = ; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
-iuclc -ixany -imaxbel
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
echoctl echoke
% ./signal-int [←]
main(): going into infinite loop, sigint_count == 3
main(): sigint_count == 3, pause() ....
^C
sigint_handler():
sigint_handler(): sigint_count == 2
main(): return from pause().  sigint_count == 2
main(): sigint_count == 2, pause() ....
^C
sigint_handler():
sigint_handler(): sigint_count == 1
main(): return from pause().  sigint_count == 1
main(): sigint_count == 1, pause() ....
^C
sigint_handler():
sigint_handler(): exit() ... 
% []
----------------------------------------------------------------------
stty -a で、 intr がどのキーに割り当てられているかを調べる。この場合、 ^C に割り当てられている。./signal-int を実行し、^C を3回押している。

^C の代りに、別の端末から kill コマンドを使って割込みを発生さ せる方法もある。


----------------------------------------------------------------------
% kill -INT PID [←]
----------------------------------------------------------------------
ここで、PID は、./signal-int のプロセスの PID (Process ID)である。PID は、ps コマンドで調べる。

◆EINTR

read(), write(), accept(), select(), poll() などのシステムコールを実行 中にソフトウェア割込みが生じると、システムコールがエラーになる。errno は、EINTR になっている。

errno が EINTR の時、システムコールを再実行する必要がある場合が多い。

◆再入可能(reentrant)

一つの関数を実行の途中で、割込みが行われて、もう一度同じ関数が呼ばれた 時に何が起るか。

そのような場合でも、問題なく動く関数を、再入可能な関数(reentrant function)という。

引数やauto変数だけを使った関数は、再入可能になる。

extern や static を使った関数は、再入可能でないことがある。

次のような関数は、リエントラントではない。

シグナル・ハンドラの中では、リエントラントな関数を使うようにする。

◆タイマ割込み

オペレーティング・システムのカーネルでは、定期的(1/100秒)な割込みを使っ て、プロセスのスケジューリングやTCP/IP の時間切れ再転送の処理を行って いる。定期的な割込みは、タイマ割込みと呼ばれる。タイマ割込みのためのハー ドウェアが付いている場合が多いが、以前は、電源の交流をトリガに使ってい るシステムもあった。

タイマ割込みをソフトウェアで実現したものが、setitimer()システムコール である。

◆イベント駆動

ウインドウ・システムのプログラムやゲームのプログラムは、イベント駆動の プログラムになっている場合が多い。イベント駆動のプログラムでは、あるイ ベント(たとえばボタンが押された)が発生したら、ある関数を実行するよう になっている。

ゲームで、人間がボタンを押さなくても進んで行くところは、タイマ割込みに よる。

■メモリ・マップ

図? のように, Unixのプロセスのメモリは、はテキストセグメント, データセグメ ント, スタックセグメントに分類される。ここでセグメントとは、連続した メモリの領域のことである。

図? プロセスのアドレス空間

図? プロセスのアドレス空間

◆テキストセグメント

 機械語命令を置くためのセグメンを テキスト・セグメント ( text segment, ) という。このセグメントは、普通、読み出し専用になっていて, 同じロードモ ジュールのプロセスの間で共有される。例えばシェルは複数のユーザが同時に 使うことが多いが、この場合、テキストセグメントのためのメモリは1セット だけでよい。

機械語を書き換えながら実行するプログラムは、最近は行儀が悪いとされてい る。キャッシュの関係で特別な操作をしないと動かない。

◆データセグメント

データ・セグメント ( data segment, ) は、データを置く領域である. C言語の 静的変数(static)大域変数(extern) malloc() で割り当てたヒープ上の変数は、 このセグメントに置かれます。 ( 自動変数(auto) は、次のスタック・セグメントに置かれる。 )  データセグメントは, 次の3つにわかれています.
データ
静的変数や大域変数のうち、初期値つき変数が置かれるところ
BSS
静的変数や大域変数のうち、初期値を指定しない(初期値が0)が置かれるところ
ヒープ(heap) malloc() で確保する変数が置かれるところ
 データの初期値は、実行形式ファイルに含まれている. 例えばC言語で

static int x=100 ;
と書くと xはデータセグメントに割りつけられ, 実行形式ファイルに100という整数のビットパタンが含まれる。

 BSSとは初期値を指定しない変数が置かれる場所である. C言語で


static int x ;
と書くと yはBSSに割りつけられる。実行形式のファイルに は、BSSの領域はない。これは実行時にカーネルが領域を割り当て、内容 を0に初期化します。

 ヒープとは実行時に大きさが決まる変数を置くための領域である. C言語で


char *p ;
    ...
    p = malloc( 100 );
とすると100バイトの領域がヒープ上に確保される。

ヒープ領域の大きさは、brk() システム・コールや sbrk() システム・コールで変えることができる。これらの システム・コールは、malloc() ライブラリ関数から呼び出さ れる。

◆スタックセグメント

スタック・セグメント ( stack segment, ) とはスタックを置くためのメモリ領域である。C言語の 自動変数(auto variable)、 変数は, ここに置かれる.

 C言語で


main()
{
    auto int z;
}
とすると, 4バイトの領域がスタックセグメントに割りつけられる。 (autoというキーワードは、普通省略される。スタックセグメ ントは、普通、0xffffff (32ビットのアドレス空間の場合)からからアドレス が小さくなるほうに向かって伸びていく。この部分は、関数呼び出しが続くと 伸びていき、割り当てられたメモリよりも多くなると、カーネルが自動的に拡 張する。

◆mainの引数と環境変数

 UNIXではプログラムを実行するときに, 引数(argument)環境変数(environment variable) が渡される。 C言語では、次のようにmain()関数が3つの引数で呼び出される ようになっている。

main( int argc, char *argv[], *envp[] )
{

}
ここで、 argc が、引数の数(argvの数) , argv が、引数のベクタの先頭番地、 envp が環境変数のベクタの先頭番地である。この引数と環境変数は、スタックセグ メントのスタックの底、 図? では上(アドレスが大きいところ)にある。

◆共有ライブラリ

共有ライブラリは、ヒープとスタック・セグメントの間にある。 どの番地に割り当てられるかは、システムに依存する。

■変数の番地、メモリ・マップ

----------------------------------------------------------------------
   1:	/*
   2:	        vaddr-print.c -- 変数の番地をしらべるプログラム
   3:	        ~yas/syspro/proc/vaddr-print.c
   4:	        Start: 1997/05/19 22:58:49
   5:	*/
   6:	#include <stdlib.h>
   7:	
   8:	void recursive( int n );
   9:	
  10:	int x1=1 ;
  11:	int x2 ;
  12:	
  13:	extern int etext, edata, end ;
  14:	
  15:	main( argc,argv,envp )
  16:	    int argc ;
  17:	    char *argv[] ;
  18:	    char *envp[] ;
  19:	{
  20:	    int x3 ;
  21:	    char *x4p ;
  22:	
  23:	        printf("&main    == 0x%08x (text)\n",&main );
  24:	        printf("&etext   == 0x%08x (text)\n",&etext );
  25:	        printf("&edata   == 0x%08x (data)\n",&edata );
  26:	        printf("&end     == 0x%08x (data)\n",&end );
  27:	
  28:	        printf("&argv[0] == 0x%08x (stack)\n",&argv[0] );
  29:	        printf(" argv[0] == 0x%08x (stack)\n", argv[0] );
  30:	        printf("&envp[0] == 0x%08x (stack)\n",&envp[0] );
  31:	        printf(" envp[0] == 0x%08x (stack)\n", envp[0] );
  32:	
  33:	        printf("&x1      == 0x%08x (data)\n",&x1 );
  34:	        printf("&x2      == 0x%08x (bss)\n",&x2 );
  35:	        printf("&x3      == 0x%08x (stack)\n",&x3 );
  36:	        x4p = malloc( 10 );
  37:	        printf("x4p      == 0x%08x (heap)\n",x4p );
  38:	        x4p = malloc( 10 );
  39:	        printf("x4p      == 0x%08x (heap)\n",x4p );
  40:	        recursive( 3 );
  41:	}
  42:	
  43:	void recursive( int n )
  44:	{
  45:	    int x5 ;
  46:	        printf("&x5      == 0x%08x (stack,%d)\n",&x5,n );
  47:	        if( n<=0 )
  48:	            return;
  49:	        recursive( n-1 );
  50:	}
----------------------------------------------------------------------

実行例。
----------------------------------------------------------------------
% cp ~yas/syspro/proc/vaddr-print.c . [←]
% make vaddr-print [←]
cc     vaddr-print.c   -o vaddr-print
% ./vaddr-print  [←]
&main    == 0x08048490 (text)
&etext   == 0x0804866e (text)
&edata   == 0x08049910 (data)
&end     == 0x0804992c (data)
&argv[0] == 0xbfffd984 (stack)
 argv[0] == 0xbffff966 (stack)
&envp[0] == 0xbfffd98c (stack)
 envp[0] == 0xbffff974 (stack)
&x1      == 0x08049830 (data)
&x2      == 0x08049928 (bss)
&x3      == 0xbfffd914 (stack)
x4p      == 0x08049938 (heap)
x4p      == 0x08049948 (heap)
&x5      == 0xbfffd8f4 (stack,3)
&x5      == 0xbfffd8d4 (stack,2)
&x5      == 0xbfffd8b4 (stack,1)
&x5      == 0xbfffd894 (stack,0)
% size vaddr-print [←]
   text    data     bss     dec     hex filename
   1815     240      28    2083     823 vaddr-print
% []
% ldd vaddr-print [←]
        libc.so.6 => /lib/i686/libc.so.6 (0x4002d000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
% []
----------------------------------------------------------------------
main() のような関数の番地は、0 に近い場所(0から0xffffffffの 間の 1/32 辺り)にある。スタックは、0 から遠い場所にある。

引数や環境変数は、スタックの底(0から遠い方)にある。

初期値付きの変数は、&edata 以下にある。

初期値なしの変数は、&edata と&end の間にある。

malloc() で確保したデータの番地は、&end より大きい。free() がなければ、普通は、malloc() する度に大きくなるが、この性質は保証され ていない。

スタック上の変数は、再帰呼出しの度に、番地が小さくなる。

■練習問題と課題

★練習問題 57 UDP/IPで送れるデータの大きさの上限

UDP/IP では、一度に送ることができるデータの大きさにには、実装上の制限 が付いている。これがいくつかを調べるプログラムを作りなさい。

★練習問題 58 udprelay

UDP/IP のデータを中継するようなプログラムを作りなさい。 単方向だけでなく、双方向で中継するようにしなさい。

このようなプログラムの例として、udprelay と呼ばれるプログラムがある。

★練習問題 59 UDP/IPでのアクセス制御

UDP/IP のサーバ、または、中継プログラムで、クライアントの IP アドレス によってアクセスを許したりエラーを発生させたりしなさい。

★練習問題 60 UDP/IPによるサービスの利用

次の UDP/IP で提供されているサービスを利用するクライアントを作成しなさい。
daytime (13, Daytime Protocol)
rfc867.txt
time (37, Time Protocol)
rfc868.txt

★練習問題 61 kill コマンドによるシグナルの発生

signal-int.c のプロ グラムを動かし、kill コマンドを使って SIGINT (kill -INT)でシグナルを送 りなさい。そして、キーボードから intr のキー^Cを打った時と動 作を比較しなさい。

ヒント:kterm を2つ開いて、片方でこのプログラムを動かし、片方で kill コマンドを動かす。kill 引数として必要な PID は、ps コマンドで調べる。

★練習問題 62 killコマンド

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

★練習問題 63 ^Cで死なないプロセス

SIGINT シグナルを発生させても終了しないプロセスを 作りなさい。

ヒント:そのシグナルを無視(SIG_IGN)するように設定する。

★練習問題 64 sigaction()の利用

signal() の代りに、sigaction() という、新しいインタフェースを使って割 込みハンドラを登録してみなさい。

★練習問題 65 setitimer()の利用

setitimer() を使うと、SIGALRM という種類の割込みを定期的に発生させるこ とができる。これを確かめなさい。

★練習問題 66 printfの番地

printf 関数の番地を調べなさい。

★練習問題 67 引数の番地

関数の引数の番地を調べなさい。auto変数と比較しなさい。