文字列とパイプ

システム・プログラム

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

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

■復習

■今日の目標

■整数の符合化とあつかい

コンピュータの(メモリ)の中で整数がどのように符合化されているか。基本 的には、2進数(ビットの並び)だが、ビット数やバイトオーダの問題がある。

◆バイト・アドレッシング

メモリには、1バイトごとに番地が振られている。 (1ワードごと、1ビットごとにアドレスを振る方法も考えられる。)

◆C言語の整数

C言語では、いろいろなビット数(バイト数)の整数が使える。
char
8 ビット
short int
16 ビット
int
32 ビット (処理系依存)
long int
32 ビット
char も、8ビット(1バイト)の整数であることに注意しなさい。 unsigned を付けると符合無しになる。

Java では、char は、整数型とはまったく違う。相互に代入できない。 (Java の char は、内部的には、16 ビット。Unicodeで符号化される。)

int i ;
char c;
    c = i ; /* C では OK. Java ではエラー */

◆バイト・オーダ

複数バイトの整数をメモリに置く時に、連続した複数番地のメモリを使う。 どういう順番で置くかで2つの方法がある。 数の大小比較の時に、最も効いてくる(most significant)なビット(を含むバ イト、上位バイト)をどこに置くかで、次の2つの方法がある。
ビッグエンディアン。番地が小さい方に上位バイトをを置く。
リトルエンディアン。番地が小さい方に下位バイトを置く。
例:0x12345678の置き方。

バイト・オーダ(その1)

図? バイト・オーダ(その1)

注意:CPUのレジスタの中では、バイト・オーダは関係ない。

バイト・オーダ(その2)

図? バイト・オーダ(その2)

◆バイト・オーダを調べるプログラム

   1:	/*
   2:	        byte-order.c -- バイト・オーダを調べるプログラム
   3:	        ~yas/syspro/cc/byte-order.c
   4:	        Start: 2001/04/22 19:01:41
   5:	*/
   6:	
   7:	int i=0x12345678 ;
   8:	
   9:	main()
  10:	{
  11:	    char *p,c ;
  12:	        printf("&i == 0x%x\n",&i );
  13:	        p = (char *)&i ;
  14:	        printf(" p == 0x%x\n",p );
  15:	        c = *p ;
  16:	        printf(" c == 0x%02x\n",c );
  17:	        printf("{0x%02x,0x%02x,0x%02x,0x%02x} \n",p[0],p[1],p[2],p[3] );
  18:	}
実行結果。デバッガ gdb で変数の番地を調べて、その番地の内容を x コマン ドで表示する。

% cc -g byte-order.c -o byte-order [←]
% ./byte-order  [←]
&i == 0x80495f0
 p == 0x80495f0
 c == 0x78
{0x78,0x56,0x34,0x12} 
% gdb byte-order [←]
GNU gdb Red Hat Linux (5.1-0.71)
Copyright 2001 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux"...
(gdb) r[←]
Starting program: /home/lab/Denjo/yas/syspro-2002/cc/byte-order 
&i == 0x80495f0
 p == 0x80495f0
 c == 0x78
{0x78,0x56,0x34,0x12} 

Program exited with code 027.
(gdb) p &i[←]
$1 = (int *) 0x80495f0
(gdb) x 0x80495f0[←]
0x80495f0 <i>:  0x12345678
(gdb) x/4b 0x80495f0[←]
0x80495f0 <i>:  0x78    0x56    0x34    0x12
(gdb) q[←]
% []

◆符合拡張

C言語では、関数呼出しの時、char も short も、int へ拡張する。符合付き なら、符合をつけたまま拡張する。
main()
{
   char c = 'A' ;
    f( c ); /* ここで符合拡張が行われる */
}

f(char c) /*int c でも受けられる。*/
{
   ....
}

◆C言語の&演算子

C言語の「&」には、2つの意味がある。
x & y
x と y のビット単位の AND
& v
変数 v の番地

◆番地(ポインタ)

read() システム・コールや write() システム・コールでは、たとえ1バイト でも「番地」を与える必要がある。
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
次のプログラムは、間違いである。
main()
{
    void *buf ;
    read( 0, buf, 1 );
}
ポインタ buf 変数は、番地を保持する。ポインタを使う時には、どの番地を 指しているかが大事である。初期化されていない。ポインタも、どこかの番地 を差している。そのどこかは、メモリが割り当てられていないかもしれない。

ポインタは、必ず次のいずれかの方法で初期化して使う。

◆浮動小数点

小数の符合化の方法にもいくつかの方法がある。詳しくは、数値計算関連の講 義で話がでる。重要なことは、浮動小数点の場合は、計算に誤差が生じること である。整数では、オーバーフローを除いて誤差は生じない。

浮動小数点以外の小数の符合化の方法も考えられる。

◆数の入出力

Unix の read(), write() システムコールは、「バイト列」を読み書きするも のである。Unix で数を読み書きするには、次の2つの方法がある。
バイナリのまま。
int なら4バイト読み書きする。ファイル向き。RPC文化。 バイトオーダに気をつける。
(ASCII)文字列に変換する。
キーボード向き。telnet 文化。

キーボードで打てるのは、ASCII 文字の並びである。キーボードから int を 読みたい時には、文字の並びとしてまず読み込み(fgets())、次に、 文字の並びから整数に変換する(atoi(), strtol(), sscanf())

scanf() は、人間が打つ、間違い(意図的な攻撃)を含む可能性がある場所で 使うべきではない。

OSの中には、キーボードからASCII 文字の並び('0' から'9')を打ち込むと、OSの中で整に変換する機能が あるものもある。Unix には、そのような機能はない。文字の並びではなく数 が必要ならば、ライブラリ関数で変換する。

バイトオーダを変換するには、次のような関数を使う。

YHM_Esape(#include )
unsigned long  int htonl(unsigned long  int hostlong );
unsigned short int htons(unsigned short int hostshort);
unsigned long  int ntohl(unsigned long  int netlong  );
unsigned short int ntohs(unsigned short int netshort );
ここで、n は、network、h は、ホスト、s は short、l は、long を意味する。 ネットワークの変りに、ファイルでもよい。ファイルやネットワークに出力す る時には、htonl() や htons() で標準系(ビッグエンディアン)に変換する。 入力の時は、逆に ntohl() や ntohs() で元にもどす。

■文字の符号化

文字の符号化とは、文字とビット列(または整数)を対応させることであ る。対応のさせかたには何種類もある。
A	1
B	2
C	3
...
Z	26
モールス符合。

ポケットベル・コード。

コンピュータでよく使われる文字集合と文字の符号化の方法

厳密には、文字集合と文字コードは違う。JIS X 0208-1983符号もEUC-jpも、 Shift-JIS も文字集合は同じなので、相互に変換できる。 Unicode と JIS X 0208-1983 は、完全には相互に変換できない。 Unicode の符号化の方法には、UTF-1, UTF-7, UTF-8 などの種類がある。

数の値と数を画面に文字として表示するために使う符合が違うことに注意する。 数の0と文字の0は違う。

◆ISO Standard ISO8859-1 Latin-1

Latin-1コード表

8ビット符号。 20(16進)から7F(16進)までは、ASCII と同じ。 西ヨーロッパでよく使われている。

参考:

http://www.asahi-net.or.jp/~sd5a-ucd/rec-html401j/sgml/entities.html,http://www.asahi-net.or.jp/~sd5a-ucd/rec-html401j/sgml/entities.html
HTML 4.01仕様書/24 HTML 4の文字実体参照

◆JIS X 201

JIS X 201 コード表

◆漢字コード

JIS漢字コード表の一部

漢字1文字を、7ビットと7ビットの組でで表す。それぞれ 21〜7E に入るよ うになっている。16進で2121から747Eまでの範囲にある。ただし、半分は空い ている。

参考:

http://www.hlla.is.tsukuba.ac.jp/~yas/classes/ipe/nitiniti2-enshu-1996/1996-11-18/kanji-code.html
漢字コードに関する解説

■C言語での文字列

「''」と「""」の違いを理解すること。
  int x ;
  x = 'A' ;  /* (1) */
  x = "A" ;  /* (2) */

char の配列として表現される。連続番地に置かれ、0終端されている。ダブ ルクォート「""」で囲むと、名前がない配列が作られ、その先頭番地が入る。

C言語のプログラムの中に 漢字を「""」で囲んだ時には、EUC だと比較的そのまま通る。 JIS だと、「"」や「\」と重なるバイトを持つ漢字が現れた時に問題が生じる。

ファイルから読み込む時には、0終端という約束に従っていれば、どんな 文字コードでも扱えるとも言える。

◆文字列と文字の配列

C言語で文字列は、0で終端された、0 以外のバイトの並びである。ASCII 文 字 だけ考えれば簡単だが、漢字やその他の言語の文字まで考えると、符号化 の問題があるので、本当の文字列の扱いは難しい。

C言語で "" で括った文字列を指定すると、名前がない charの配列が作られ、 その先頭番地が返される。配列の内容は、指定された文字と、最後に文字列の 終端を意味する 0 が付く。表面的に見える文字の数より 1 バイト多いことに 注意する。

   1:	/*
   2:	        string-array.c -- 文字列と配列
   3:	        ~yas/syspro/string/string-array.c
   4:	        Start: 2002/04/21 18:24:34
   5:	*/
   6:	
   7:	char h1[] = { 'h','e','l','l','o',0 };
   8:	
   9:	main()
  10:	{
  11:	    int i ;
  12:	        printf("%s\n",h1 );
  13:	        printf("0x%x, 0x%x\n",h1,"hello" );
  14:	        for( i=0 ; i<5 ; i++ )
  15:	        {
  16:	            printf("[%c]", h1[i] );
  17:	        }
  18:	        printf("\n");
  19:	        for( i=0 ; i<5 ; i++ )
  20:	        {
  21:	            printf("[%c]", "hello"[i] );
  22:	        }
  23:	        printf("\n");
  24:	}

配列なので、"hello"[i] のようなこともできる。

printf() の %x, %s, %c の意味に注意する。

%x
4バイトの値を数だと思って、16進表記の文字コードの並び変換して出力する。
%s
4バイトの値を番地だと思って、その番地から始まる内容を、0が来るまで出力する。
%c
4バイトの値のうち、下位1バイト分だけをそのまま出力する。 (関数呼出しで引数に渡す時に、1バイトの整数も2バイトの整数も自動的に 4バイトに拡張される。)

実行結果。


% cp ~yas/syspro/string/string-array.c . [←]
% make string-array [←]
cc     string-array.c   -o string-array
% ./string-array  [←]
hello
0x80495b8, 0x804858c
[h][e][l][l][o]
[h][e][l][l][o]
% []
文字列と文字の配列

◆文字列操作のためのAPI

string(3) に一覧がある。よく使うものは、次のようなもの。

#include <string.h>

文字列の長さを調べる(終端の 0 は、長さに含まれない)
    size_t strlen(const char *s);

文字列のコピー
    char *strcpy(char *dest, const char *src);		  // 使わない方がよい
    char *strncpy(char *dest, const char *src, size_t n); // 使わない方がよい
    char *strdup(const char *s);

文字列の連結
    char *strcat(char *dest, const char *src);		  // 使わない方がよい
    char *strncat(char *dest, const char *src, size_t n); // 使わない方がよい

文字列の比較
    int strcmp(const char *s1, const char *s2);
    int strncmp(const char *s1, const char *s2, size_t n);
    int strcasecmp(const char *s1, const char *s2);
    int strncasecmp(const char *s1, const char *s2, size_t n);

str"n"cpy() は、n バイトだけコピーする。n より短かったら、残りの部分に 0 を埋める。str"n"cat() も同様である。固定長の構造体の内部の文字列を初 期化する場合には使えるが、それ以外にはあまり使い道がない。

str"case"cmp では、大文字(upper case)と小文字(lower case)の区別をしな い。

strcpy() や strcat() は、バッファ・オーバーフローが起きないように使う には難しい。OpenBSD, FreeBSD, SunOS 5.8 には、より優れた API がある。

     size_t strlcat(char *dst, char *src, size_t dstsize);
     size_t strlcpy(char *dst, char *src, size_t dstsize);
strlcat() は、strncat() と似ているが、最後に 0 を埋めることはない。

strcpy(), strcat() の代りに次の関数も便利である。

#include <stdio.h>
    int sprintf(char *str, const char *, ...);
    int snprintf(char *str, size_t size, const  char  *,...);
#include <stdag.h>
    int vsprintf(char *str, const char *, va_list ap);
    int vsnprintfvsnprintf(char *str, size_t size, const  char *, va_list ap);
その他
文字列中の文字や文字列の検索
    char *strchr(const char *s, int c);
    char *strrchr(const char *s, int c);
    char *strstr(const char *haystack, const char *needle);
    char *index(constchar*"s, int c);
    char *rindex(const char *s, int c);

文字列中のトークンへの分解
    char *strtok(char *s, const char *delim);

その他
    int strcoll(const char *s1, const char *s2);
    size_t strcspn(const char *s, const char *reject);
    char *strfry(char *string);
    char *strpbrk(const char *s, const char *accept);
    char *strsep(char **stringp, const char *delim);
    size_t strspn(const char *s, const char *accept);
    size_t strxfrm(char *dest, const char *src, size_t n);

◆バッファ・オーバーフロー

文字列のバッファの大きさに注意する。 以下の例は、悪い例なので真似をしてはいけない。
   1:	/*
   2:	        string-cpycat.c -- 文字列のコピーと結合(悪い例)
   3:	        ~yas/syspro/string/string-cpycat.c
   4:	        Start: 2002/04/21 23:02:39
   5:	*/
   6:	
   7:	#include <stdio.h> /* stderr */
   8:	#include <stdlib.h> /* malloc() */
   9:	#include <string.h> /* strcpy(), strcat(), strlen() */
  10:	
  11:	main()
  12:	{
  13:	    char buf[100];
  14:	        strcpy(buf,"hello");
  15:	        strcat(buf,",world");
  16:	        strcat(buf,"\n");
  17:	        if( strlen(buf) >= 99 ) /* too late */
  18:	        {
  19:	            fprintf(stderr,"buffer overflow\n");
  20:	            exit( 1 );
  21:	        }
  22:	        printf("%s",buf );
  23:	        free( buf );
  24:	}

文字列と文字の配列

注意:この例は、悪い例なので真似をしてはいけない。

strcpy() で初期化して、strcat() で後ろに付け加えていく。

100 バイトのバッファでは、buf[0] からbuf[98]まで文字のデータが入れられる。 最後のbuf[99] には、文字ではなく0 を入れる。buf[100] は使ってはいけない。 (次の別のデータが入っている。)

strlen(buf) が 100 を越えた時には、使ってはいけない場所を使ってしまった ことを意味する。これを、「バッファ・オーバーフロー」という。 そもそも事後にチェックするのは、本当は遅い。そもそも、バッファ・オーバー フローが起きないように気を付けながらプログラムを書くべきである。

◆snprintf()

strcpy(), strcat() の代りに snprintf() が便利な場合がある。
   1:	/*
   2:	        string-snprintf.c -- snprintf を使った文字列のコピーと結合
   3:	        ~yas/syspro/string/string-snprintf.c
   4:	        Start: 2002/04/21 23:02:39
   5:	*/
   6:	
   7:	#include <stdio.h> /* stderr, snprintf() */
   8:	#include <stdlib.h> /* malloc() */
   9:	
  10:	main()
  11:	{
  12:	    char buf[100];
  13:	        if( snprintf(buf,100,"hello%s\n",",world") >=sizeof(buf) )
  14:	        {
  15:	            fprintf(stderr,"buffer overflow\n");
  16:	            exit( 1 );
  17:	        }
  18:	        printf("%s",buf );
  19:	}
  20:	
snprintf() は、第1引数に、バッファの番地、第2引数にバッファの長さ(最 後の0を含む)をとる。第3引数以降は、printf() と同じである。snprintf() では、けっしてバッファ・オーバーフローは起きない。起きそうになると、負 の数を返す。(成功すると、バイト数(最後の0は含まない)を返す。)

snprintf() は、%s 以外に %d や %c も使える。

注意:Linux (Glibc を使っている)では、snprintf() の仕様が変更された。 古い仕様(glibc 2.0.6以前)では、snprinf() は、エラーが起きると -1 を返 す。新しい仕様(glibc 2.1以降)では、snprintf() は、必要なバイト数、すな わち、バッファが無限大であるときに書き込まれる文字列の長さ(最後の0を除 く)を返す。これは、strlen() が返すものと同じである。従って、新しい snprintf() では、結果とバッファの大きさを比較して正確に書き込まれたか を検査する。(snprintf() は、決してバッファ・オーバーフローを起こすこと はないが、バッファが足りない時には意図していない結果が保存されているこ とになる。)

◆snprintf()によるstr*() の置き換え

strcpy(dst,src)
snprintf(dst,destsize,"%s",src)
strcat(dest,src)
snprintf(buf,bufsize,"%s%s",dest,src) (考え方の転換が必要)

◆Pascal文字列

Pascalというプログラミング言語では、文字列を表すのに、0 で終端させるの ではなくて、データの先頭に文字列の長さを置く。

■mainの引数 argv の構造

■Javaの文字と文字列

内部的には、文字型(char)は、 16 ビットで、Unicode で符合化された値を保 持する。

◆文字の Unicode の値

   1:	/*
   2:	        CharValue.java -- 文字の Unicode の値を表示する
   3:	        ~yas/syspro/string/CharValue.java
   4:	*/  
   5:	
   6:	class CharValue {
   7:	    static char h1[] = { '0','A','a','漢','字' };
   8:	    public static void main( String args[] )
   9:	    {
  10:	        for( int i=0 ; i<05 ; i++ )
  11:	        {
  12:	            char ch = h1[i] ;
  13:	            int ch_intval = ch ;
  14:	            String dstr = Integer.toString( ch_intval, 10 );
  15:	            String xstr = Integer.toString( ch_intval, 16 );
  16:	            stdout.println(ch+" "+dstr+" 0x"+xstr);
  17:	        }
  18:	    }
  19:	    static java.io.BufferedReader stdin = 
  20:	        new java.io.BufferedReader( new java.io.InputStreamReader(System.in) );
  21:	    static java.io.PrintStream stdout = System.out;
  22:	    static java.io.PrintStream stderr = System.err;     
  23:	}
実行結果。

% javac CharValue.java [←]
% java CharValue [←]
0 48 0x30
A 65 0x41
a 97 0x61
漢 28450 0x6f22
字 23383 0x5b57
% []

◆文字列からの個々の文字の取り出し

String クラスの charAt() メソッドを使う。

~yas/syspro/string/StringCharValue.java

    static String s1 = "0Aa漢字";
...
	    char ch = s1.charAt(i);

◆ファイルからの文字の入力

FileInputStream() では、バイト配列で入力を行える。InputStreamReader() を使うと、文字コードを変換しながら、文字単位で読み込むことができる。入 力された時点で、文字コードは Unicode になっている。入力ファイルの文字 コードは、InputStreamReader() の第2引数で指定するこができる(下の例で は使っていない)。
InputStreamReader(InputStream in)
デフォルトの文字エンコーディングを使う InputStreamReader を作成します。
InputStreamReader(InputStream in, Charset cs)
与えられた文字エンコーディングを使う InputStreamReader を作成します。
InputStreamReader(InputStream in, CharsetDecoder dec)
与えられた文字エンコーディングデコーダを使う InputStreamReader を作成します。
InputStreamReader(InputStream in, String charsetName)
指定された文字エンコーディングを使う InputStreamReader を作成します。
行単位(readLine())で入力したり、文字単位でも効率をよくするにはバッファ (BufferedReader)をかませる。
   1:	/*
   2:	        FileCharValue.java -- ファイルの中の文字の Unicode の値を表示する
   3:	        ~yas/syspro/string/FileCharValue.java
   4:	*/  
   5:	
   6:	import java.io.*;
   7:	
   8:	class FileCharValue {
   9:	    public static void main( String args[] ) throws java.io.IOException
  10:	    {
  11:	        if( args.length != 1 )
  12:	        {
  13:	            stderr.println("Usage: % java FileCharValue filename");
  14:	            System.exit( 1 );
  15:	        }
  16:	        String filename = args[0];
  17:	        InputStream fis = new FileInputStream( filename );
  18:	        Reader isr = new InputStreamReader( fis );
  19:	        BufferedReader br = new BufferedReader( isr );
  20:	        int ch_intval ;
  21:	        while( (ch_intval=br.read()) >= 0 )
  22:	        {
  23:	            char ch = (char)ch_intval ;
  24:	            String dstr = Integer.toString( ch_intval, 10 );
  25:	            String xstr = Integer.toString( ch_intval, 16 );
  26:	            stdout.println(ch+" "+dstr+" 0x"+xstr);
  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:	}
実行結果。

% javac FileCharValue.java [←]
% cat file.text [←]
0Aa漢字
% od -xc file.text [←]
0000000 4130 b461 bbc1 0afa
          0   A   a 264 301 273 372  \n
0000010
% java FileCharValue file.text [←]
0 48 0x30
A 65 0x41
a 97 0x61
漢 28450 0x6f22
字 23383 0x5b57

 10 0xa
% []

◆文字と文字列の出力

(文字コードを指定して)文字や文字列をを出力するには、 OutputStreamWriter() を使う。

◆Unicodeの問題

◆数の入出力(Java)

バイナリのまま。
DataInputStream の readInt() や DataOutputStream の writeInt() を使う。 バイトオーダは、ビッグエンディアンになる。
(ASCII)文字列に変換する。
文字列を読み込み、変換する。Integer の parseInt() やtoString()を 使う。

◆文字列操作のためのAPI

次のようなものがよく使われる。
コンストラクタ
    String s = ""; // String s = new String();
    String(byte[] bytes) 
    String(String original)
    String(StringBuffer buffer)

文字列の連結、取り出し
    s1 = s2 + s3; // s1 = s2.concat(s3);
    String substring(int beginIndex) 
    String substring(int beginIndex, int endIndex) 

文字列の長さを調べる
    int length()

文字列の比較
    int compareTo(String anotherString)
    int compareToIgnoreCase(String str)

文字列中の文字や文字列の検索
    char charAt(int index)
    int indexOf(int ch)
    int indexOf(int ch, int fromIndex)
    int indexOf(String str)
    int indexOf(String str, int fromIndex) 
    int lastIndexOf(int ch)
    int lastIndexOf(int ch, int fromIndex) 
    int lastIndexOf(String str)
    int lastIndexOf(String str, int fromIndex)

文字列中のトークンへの分解
    String[] split(String regex)

その他
    char[] toCharArray() 
    static String valueOf(x)
    // x は boolean, char, char[], double, float, int, long, Object
文字列を得るには、「文字列 + x」の形もよく使われる。 toString() メソッドが自動的に呼ばれる。 split() より java.util.StringTokenizer() が使いやすいことも多い。

C言語の sprintf() 相当の書式つきで文字列にするには、Format を使う。 たとえば、整数で sprintf() の %2d ならば、次のようにする。

    NumberFormat nf = NumberFormat.getInstance();
    nf.setMaximumIntegerDigits(2);
    int n ;
    String s = nf.n ;

◆StringBuffer

Java の String は、GC (Garbage Collection) が頻発する状況になると遅い。 String を new した分は、GC で回収する必要がある。StringBuffer を使えば、 減らす高速化できる。String を new しなくてよくなるので GC が減らせられ る。

次のプログラムは、

     x = "a" + 4 + "c"
次のようにコンパイルされる。
    StringBuffer sb = new StringBuffer();
    sb.append("a");
    sb.append(4);
    sb.append("c");
    x = sb.toString();
次のような操作がある。
内部の操作
    append()
    delete()
    replace()
    insert()
    setCharAt()
    deleteCharAt()

アクセス
    length() 
    toString()
    charAt()
    indexOf()
    lastIndexOf()
    length() 
    getChars()
    substring()

その他
    setLength()
    capacity() 
    ensureCapacity()
    reverse() 

■pipe() と dup()

シェルで、次のようなプログラムを動かすこと考える。
% echo "hello,world" | tr '[a-z]' '[A-Z]'
HELLO,WORLD
% []
この例では、echo のプロセスと、tr のプロセスは、パイプで接続されている。 パイプは、open() したファイルと同じようにread() したり write() したり することがでる。しかし実際には、ファイルではなく、プロセスとプロセスが データを交換する仕組(プロセス間通信の仕組)の1つである。片方のプロセ スが write() したデータを、もう片方のプロセスがread() できる。

パイプによるプロセス間通信

図? パイプによるプロセス間通信

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

バッファによるパイプの実現

図? バッファによるパイプの実現

内部的には、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() システムコール実行前

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

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

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

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

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

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

図? 親子で 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() 後

図? 親子で 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)に書き出す。

■練習問題

★練習問題 25 バイトオーダの実験

バイトオーダが異なる2つのコンピュータを使ってきちんと整数値を交換する プログラムを作成しなさい。この課題では、2つのプログラムを作成する。
write-int
整数をファイル、または、標準出力に書き出す。
read-int
整数をファイル、または、標準入力から読み込む。
この2つのプログラムを2つのバイト・オーダが異なるコンピュータ(ここで は hostA, hostB とする)でコンパイルする。合計4つのバイナリができる。

実験では、次の全ての組み合わせでデータが正確に読み書きできることを確か めなさい。

  1. hostA で write-int、hostA で read-int
  2. hostA で write-int、hostB で read-int
  3. hostB で write-int、hostA で read-int
  4. hostB で write-int、hostB で read-int
ここで2つのホストとして、1つは、Linux が動いているもの(adonis, azaleaなど)、もう1つは、vine1 (Sun SPARC) を使いなさい。

★練習問題 26 stringライブラリの仕組み

次のライブラリ関数から1つを選び、それと似たような動きをする関数を作り なさい。 ただし、既存の他のイブラリ関数を呼んではならない。strdup() に限っては、 malloc() を呼びなさい。必要ならば、strlen() 関数を自分自身で定義して利 用してもよい。最初からあるライブラリ関数と紛らわしいので、mystrlcat(), mystrlcpy() のように、頭に my を付け、名前を変えなさい。正しく動作して いることを示すような実行結果を付けなさい。

Linux や *BSD の場合、ライブラリ関数のソース・プログラムは、探せば見つかる。こ の課題では、それを利用してもよい。ただし、それらのプログラムは、他のラ イブラリ関数を呼んでいるものがある。従って、他のライブラリ関数を呼び出 さないように、人手でインライン展開(別の関数の本体を呼び出し側に書く) しなさい。

★練習問題 27 stringライブラリの利用

string ライブラリの中で、strcpy(), strcat(), strlen() 以外の関数を使う プログラムを書きなさい。

★練習問題 28 バッファ・オーバーフローを起こさないプログラム関数の利用

今まで書いたプログラムの中で、次のような関数を使っているプログラムを見 つけなさい。 そのプログラムに対して長い文字列を与えてバッファ・オーバーフローが起き ることを確かめなさい。 そのプログラムを、こららの関数を使わないように書き換え、バッファ・オー バーフローが起きないプログラムに変更しなさい。

★練習問題 29 ポインタの誤用の修正

次のプログラムは、read() システム・コールを使って画面から1バイトだけ 読み込むプログラムであるが、誤りを含んでいる。
main()
{
    char *buf ;
    read( 0, buf, 1 );
    printf("%c\n",*buf );
}
この誤りを、次の2つの方法で修正しなさい。 2つの方法の利害特質を考えなさい。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

★練習問題 36 Java 版popen()

C言語の popen() ライブラリ関数と同様に、生成したプロセスの標準入力に データを与えたり、標準からにデータを読み込んだりすることができる。それ には、java.lang.Process の getInputStream()、getOutputStream()、および、 getErrorStream() を使えばよい。それを使うようなプログラムを作成しなさい。

★練習問題 37 Java StringBufferの性能

Java で、String を多数 new するようなプログラムと StringBuffer を利用 するようなプログラムの性能を比較しなさい。
Last updated: 2004/04/19 16:59:31
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>