ブログトップ 記事一覧 ログイン 無料ブログ開設

子持ちししゃもといっしょ RSSフィード

2012-03-30

Linuxシステムコールの勉強まとめ

今年に入ってから「Linuxシステムコールの勉強」ということで16回にわたってエントリーを起こしました。

これは今をさかのぼること5年前に、このブログのサブブログで書いたエントリーです。

当時はOSのAPIに凝っていた時期でもありまして、Win32APIとLinux系のシステムコールにハマっていましたので書かれているサイトの少なそうシステムコールについて一冊の本をベースにしてまとめました。途中で出てくるサンプルも本のサンプルそのままはよくないだろうということと、もっとシステムコールの動きが楽しめるおもしろいサンプルにしたいという二つの想いからかなり手を加えました。

そんなふうに手をかけた割には3か月という短い期間で16エントリー書けたので個人的にはかなり満足しています。


さて。

ではなんでそのエントリーをこのブログに転載しているのかというと、実はそのブログを近々消そうかなと思っているためです。元々4年前から更新のないブログではあるのですが、有料サービスには登録していまして月々200円ばかりのお布施が支払われています。

その有料サービスを解除するにあたって、それだったらいっそ必要なエントリーを引っ越したら古い方は消してしまおうかと思ったわけでしてとりあえず今月中には古い方は消す方向で検討しています。


タイトル(リンク)内容
Linuxシステムコールの勉強(その1)システムコールの概要とexec()/fork()について
Linuxシステムコールの勉強(その2)fork()について
Linuxシステムコールの勉強(その3)Signalについて
Linuxシステムコールの勉強(その4)Signal Handlerについて
Linuxシステムコールの勉強(その5)Signalについて
Linuxシステムコールの勉強(その6)ファイル操作について
Linuxシステムコールの勉強(その7)効率的なファイル入出力の方法と、ファイルディスクリプタの複製について
Linuxシステムコールの勉強(その8)ioctl()を使用した端末の制御について
Linuxシステムコールの勉強(その9)ioctl()を使用した端末の制御について
Linuxシステムコールの勉強(その10)端末のCooked/RAWモードについて
Linuxシステムコールの勉強(その11)ioctl()を使った端末制御
Linuxシステムコールの勉強(その12)プロセス間通信(パイプ)
Linuxシステムコールの勉強(その13)プロセス間通信(パイプ)
Linuxシステムコールの勉強(その14)名前付きパイプを使ったプロセス間通信
Linuxシステムコールの勉強(その15)名前付きパイプの使用方法
Linuxシステムコールの勉強(その16)ソケット通信

次は前から作りたかったiPhoneアプリやAndroidアプリの作り方でもまとめようかな。

2012-03-07

Linuxシステムコールの勉強(その16)

Linuxシステムコール

Linuxシステムコール

前回はこちら


今回からソケット通信についてまとめます。ちなみにこの本はソケット通信の章が最後なのでこのシリーズもここでひとまず終わりです。というわけで張り切ってまとめます。


ソケット通信はパイプを使用したプロセス間通信を拡張したものです。接続を受け付ける側(サーバ)と接続を要求する側(クライアント)がそれぞれソケットを作成して、「プロトコル」と「通信方式」を決めて通信を行います。


まずはサーバ側の処理の流れを図にしてみました。


f:id:itotto:20120215221943p:image


listen用のソケットをsocket()で作成し、bind()でソケットに名前を付けます。そしてlisten()で接続用のソケットキューを作成し、accept()でクライアントからの接続を受け付けます。accept()を実行したタイミングで通信用のソケットが作成されて、以降はそちらのソケットで通信を行います。

# listen用のソケットはあくまで待ち受け用のソケットです


accept()以降は通常のファイル操作同様にread()/write()でデータの読み書きを実施出来ます。使い終わったらファイル同様にディスクリプタを指定してソケットをクローズします。

これがサーバ側の処理の流れです。


続いてクライアント側の流れも図にしてみました。

f:id:itotto:20120215221944p:image

サーバ同様に通信を行うためのソケットをsocket()で作成します。それをconnect()で接続して、あとはread()/write()で読み書き。それが終わったらこれまたサーバ側と同様にディスクリプタを指定してソケットをクローズします。

これがクライアントの処理の流れです。


ひとまず出てきたシステムコールについてもう少し詳しくまとめます。


/*** ソケットを作成する ***/
// #include <sys/types.h>
// #include <sys/socket.h>
// 
// 第一引数 domain   - ソケットが適用される範囲(ドメイン)を指定[後述]
// 第二引数 type     - 通信方式[後述]
// 第三引数 protocol - 第一引数で指定したドメインで有効なプロトコルファミリを指定[通常は0固定]
//
// 返却値
//  成功時 :  有効なファイルディスクリプタ
//  失敗時 : -1
int socket(int domain, int type, int protocol);


/*** ソケットに名前を付ける ***/
// #include <sys/types.h>
// #include <sys/socket.h>
// 
// 第一引数 sockfd   - 処理対象となるファイルディスクリプタ(socket()の返却値)
// 第二引数 sockaddr - アドレス[後述]
// 第三引数 addrlen  - 第二引数のサイズ
//
// 返却値
//  成功時 :  0
//  失敗時 : -1
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);


/*** ソケットキューの作成 ***/
// #include <sys/socket.h>
// 
// 第一引数 sockfd  - 処理対象となるファイルディスクリプタ(socket()の返却値)
// 第二引数 backlog - 同時接続受付数
//
// 返却値
//  成功時 :  0
//  失敗時 : -1
int listen(int sockfd, int backlog);


/*** 接続の受付を開始 ***/
// #include <sys/types.h>
// #include <sys/socket.h>
// 
// 第一引数 sockfd   - 処理対象となるファイルディスクリプタ(socket()の返却値)
// 第二引数 sockaddr - 受け取るアドレス情報[後述]
// 第三引数 addrlen  - 第二引数のサイズ
//
// 返却値
//  成功時 :  0
//  失敗時 : -1
int accept(int sockfd, struct sockaddr *my_addr, int *addrlen);



/*** 接続の要求を開始 ***/
// #include <sys/types.h>
// #include <sys/socket.h>
// 
// 第一引数 sockfd   - 処理対象となるファイルディスクリプタ(socket()の返却値)
// 第二引数 sockaddr - アドレス情報[後述]
// 第三引数 addrlen  - 第二引数のサイズ
//
// 返却値
//  成功時 :  0
//  失敗時 : -1
int connect(int sockfd, struct sockaddr *my_addr, int addrlen);

最初にsocket()の第一、第二引数について補足。


socket()の第一引数のドメインは通信に使用するソケットの範囲を指定(言い換えれば使うプロトコルファミリーを指定)。

指定出来る値プロトコル説明
AF_UNIXUNIX内部用プロトコル同一マシン内で通信を実施する
AF_INETARPAインターネットプロトコル同一マシン or ネットワーク接続された別のマシン間で通信する
AF_ISOISOプロトコル  ? 
AF_NSXeroxネットワークシステムプロトコル  ? 
AF_IMPLINKIMP"host at IMP"リンク層  ? 

第二引数の通信方式はどういった方法で通信するのかという指定。

指定出来る値通信方式の特色
SOCK_STREAMバイトストリームソケット
信頼性が高い
データの送信順序が保証される
つまりTCP
SOC_DGRAMデータグラムソケット
信頼性が低い
データが受信される順序は保証されない
つまりUDP

次にbind()で付けられるソケットの名前について補足します。

bind()で付けられるソケットの名前が何になるのかは、socket()を呼び出すときに使用したドメインによって異なります。


ドメインソケットの名前
UNIXドメイン(AF_UNIX)ファイル名
INETドメイン(AF_INET)ポート番号

最後にbind()やaccess(),connect()で使用しているアドレス情報について説明します。各システムコールで使用している構造体は


struct sockaddr

ですが、実際には指定するドメインによって使用する構造体は異なります。


ドメイン構造体の型
UNIXドメイン(AF_UNIX)struct sockaddr_un
INETドメイン(AF_INET)struct sockaddr_in

さて、では実際に struct sockaddr_un のメンバーについて調べてみます。


itotto@itotto >cat /usr/include/sys/un.h

...
struct sockaddr_un
  {
    __SOCKADDR_COMMON (sun_);
    char sun_path[108];         /* Path name.  */
  };
...

itotto@itotto >find /usr/include -name "*.h" -exec grep -H __SOCKADDR_COMMON {} \;

/usr/include/bits/sockaddr.h:#define    __SOCKADDR_COMMON(sa_prefix)  sa_family_t sa_prefix##family

itotto@itotto >cat /usr/include/bits/sockaddr.h
...
typedef unsigned short int sa_family_t;
...

itotto@itotto >

##familyのあたりがちょっと分からないのですが、何となく雰囲気としての定義は分かりました。これらの構造体をセットすればいいようです。


というわけで、これでシステムコールの学習はひとまず終了です。

本当はここまでのまとめとして、システムコールだけで作られたアプリケーションを作れたらよかったのですが、ひさしくLinux自体さわっていないのと、仕事でもプログラムをたくさんしていてあまり気乗りしないので、とりあえずはこれで終わりにしたいと思います。

もしプログラムを書く気になったらまた追記します。

2012-02-29

Linuxシステムコールの勉強(その15)

Linuxシステムコール

Linuxシステムコール

前回はこちら


名前付きパイプの使用方法の続きです。今回は名前付きパイプを使用してプロセス間でデータを送受信するプログラムを作成します。


[受信用プログラム(receiver.c)]

      1 /*********************************************************
      2  * データ受信側プログラム(receiver.c)
      3  *********************************************************/
      4
      5 #include <unistd.h>
      6 #include <sys/stat.h>
      7 #include <fcntl.h>
      8 #include <stdio.h>
      9
     10 #include "named_pipe.h"
     11
     12 int main()
     13 {
     14     int  fifo, nbyte ;
     15     char buff[BUFSIZ];
     16
     17     /* パイプ用スペシャルファイルが無ければ作成する */
     18     if (access(MYNAMEDPIPE, F_OK) == -1)
     19     {
     20         if (mknod(MYNAMEDPIPE, 0666 | S_IFIFO, 0) == -1)
     21         {
     22             perror("mknod()");
     23             return 1;
     24         }
     25         else
     26         {
     27             printf("1. パイプ(%s)を作成しました\n", MYNAMEDPIPE);
     28         }
     29     }
     30     else
     31     {
     32         printf("1. パイプ(%s)は既に存在します\n", MYNAMEDPIPE);
     33     }
     34
     35     printf("receiver : データ待ち受け中...\n");
     36
     37     /* 読取専用でパイプを開く */
     38     if ((fifo = open(MYNAMEDPIPE, O_RDONLY)) == -1)
     39     {
     40         perror("open()");
     41         unlink(MYNAMEDPIPE);
     42         return 1;
     43     }
     44
     45     /* パイプ経由で入ってきたデータを読取 */
     46     while ( (nbyte = read(fifo, buff, BUFSIZ)) > 0)
     47     {
     48         write(fileno(stdout), buff, nbyte);
     49     }
     50
     51     close(fifo);
     52     unlink(MYNAMEDPIPE);
     53
     54     if (nbyte == -1)
     55     {
     56         perror("write()");
     57         return 2;
     58     }
     59
     60     return 0;
     61 }

[送信用プログラム(sender.c)]

      1 /**********************************************************
      2  * データ送信側プログラム(sender.c)
      3  **********************************************************/
      4
      5 #include <unistd.h>
      6 #include <fcntl.h>
      7 #include <stdio.h>
      8
      9 #include "named_pipe.h"
     10
     11 int main()
     12 {
     13     int  fifo, nbyte ;
     14     char buff[BUFSIZ];
     15
     16     /* 書込専用でパイプを開く */
     17     if ((fifo = open(MYNAMEDPIPE, O_WRONLY)) == -1)
     18     {
     19         perror("open()");
     20         return 1;
     21     }
     22
     23     /* 標準入力からデータを読み込んでパイプへ書き込み */
     24     while((nbyte = read(fileno(stdin), buff, BUFSIZ)) > 0)
     25     {
     26         if (write(fifo, buff, nbyte) != nbyte)
     27         {
     28             perror("write()");
     29             close(fifo);
     30             return 2;
     31         }
     32     }
     33
     34     if (close(fifo) == -1)
     35     {
     36         perror("close()");
     37         return 3;
     38     }
     39
     40     return 0;
     41 }

[ヘッダーファイル(named_pipe.h)]

      1 #define MYNAMEDPIPE "/home/itotto/pg/c/named_pipe"

まずは受信用プログラム(receiver.c)を[端末1]で実行します。

[端末1]

[itotto@itotto ]$ gcc -o receiver receiver.c
[itotto@itotto ]$ ./receiver
1. パイプ(/home/itotto/pg/c/named_pipe)を作成しました
receiver : データ待ち受け中...

↑この状態でデータが来るまで待機します

続いて送信用プログラム(sender.c)を[端末2]で実行します。標準入力からデータを読み取って送信するように作っているので、シェルから実行したときにパイプ(シェルのパイプ機能)経由でls -alの結果を渡して見ます。


[端末2]

[itotto@itotto ]$ gcc -o sender sender.c

[itotto@itotto ]$ ls -al | ./sender

[itotto@itotto ]$

さて[端末1]に戻ってみると、ls -alの結果が表示されています。


[端末1]

[itotto@itotto ]$ ./receiver
1. パイプ(/home/itotto/pg/c/named_pipe)を作成しました
receiver : データ待ち受け中...

合計 48
drwxr-xr-x 4 itotto itotto 4096 112122:15 ./
drwxr-xr-x 4 itotto itotto 4096  62721:57 ../
-rw-r--r-- 1 itotto itotto   14 111500:30 .vimrc
drwxr-xr-x 2 itotto itotto 4096 112113:21 executable/
prw-r--r-- 1 itotto itotto    0 112122:15 named_pipe|
-rw-r--r-- 1 itotto itotto   52 112114:20 named_pipe.h
-rwxr-xr-x 1 itotto itotto 6260 112122:15 receiver*
-rw-r--r-- 1 itotto itotto 1084 112120:51 receiver.c
-rwxr-xr-x 1 itotto itotto 5505 112122:15 sender*
-rw-r--r-- 1 itotto itotto  708 112120:50 sender.c
drwxr-xr-x 3 itotto itotto 4096 112114:27 source/

[itotto@itotto ]$

これは[端末2]からパイプ経由で書き込まれたデータがあり、それが正しく表示されている事が分かります。これは前回コマンドレベルで確認したことと同じ事をプログラムで実装出来たといえます。


さて。ここまで名前付きパイプを使ってデータをやり取りする方法についてまとめてきました。その際に出てきた問題点として


    1. データを書き込んだ場合、それを読み取ってもらわない限りブロックされてしまう
    2. 読取側(receiver)を先に起動しておかないとエラーになる

1.についてはパイプを開く際(open())の第二引数 int flagsで O_NONBLOCKフラグかO_NDELAYフラグをorで指定するとノンブロッキングモード(non-blocking mode)でパイプを開く事が出来ます。ノンブロッキングモードって?という人には後で説明します。


2.についてはsender側の処理にパイプを作成する手順が抜けてるのが原因です。ちなみにエラーはこんな感じで出ます。


[itotto@itotto ]$ls -al | ./sender 
open(): No such file or directory

[itotto@itotto ]$

これについてはソースを直します。


話を戻して。

ノンブロッキングモードについてですが、このモードは名前のとおり、処理がブロックされないモードです。つまり、前回receiverを実行した時のようにデータが送信されてくるのを待つような事はしません。つまりこのような動きになります。


>|sh|
[itotto@itotto ]$ ./receiver
1. パイプ(/home/itotto/pg/c/named_pipe)を作成しました
receiver : データ待ち受け中...

[itotto@itotto ]$ 
↑すぐにプロンプトに戻ります。

データが来るまでウェイトさせるのか、それとも受信側ではなく送信側を待たせるべきなのかどうかを判断し、受信側O_NONBLOCKフラグを立てるのかどうかを考えることが大事です。


匿名パイプや名前付きパイプについては今回で終了です。次回からはソケット通信についてまとめます。


次へ進む

2012-02-22

Linuxシステムコールの勉強(その14)

Linuxシステムコール

Linuxシステムコール

前回はこちら


今回から名前付きパイプを使ったプロセス間通信についてまとめます。

前回までやっていた匿名パイプはファイルディスクリプタを介してパイプへデータを書込んだり、読み込んだりしていました。つまりパイプには名前がありませんでした。

今回からまとめる名前付きパイプはスペシャルファイルと呼ばれる特殊なファイルを介してデータをやりとりします。ファイルには当然名前があり、その名前を使用してデータを読み書きするので「名前付き」とわざわざ呼んでいるようです。


名前付きパイプを利用する方法としては以下の方法があります。


1. mknodコマンドを利用した方法

mknodというコマンドは名前付きパイプに限らず、スペシャルファイルを作成するためのコマンドです。


[itotto@itotto ]$ \ls -l
合計 8
drwxr-xr-x 2 itotto itotto 4096 112001:04 executable/
drwxr-xr-x 2 itotto itotto 4096 112001:04 source/

[itotto@itotto ]$ mknod named_pipe p

[itotto@itotto ]$ \ls -l
合計 8
drwxr-xr-x 2 itotto itotto 4096 112001:04 executable/
prw-r--r-- 1 itotto itotto    0 112101:47 named_pipe|
drwxr-xr-x 2 itotto itotto 4096 112001:04 source/

[itotto@itotto ]$

named_pipe という名前でファイルが作成されています。さらに詳しく見てみると


 prw-r--r-- 1 itotto itotto    0 112101:47 named_pipe|
↑pというのがファイルの種類がパイプである事を示しています

となっています。ちなみに上記のファイル名の末尾に|が付いているのもパイプである事を示唆しています。


2. mkinfoコマンドを利用した方法

mkfifoはその名のとおり、fifoをmakeするコマンドです。つまりパイプ作成専用コマンドです。使い方はほとんどmknodと同じですが、専用コマンドですので引数を指定する必要がありません。


[itotto@itotto ]$ \ls -l
合計 8
drwxr-xr-x 2 itotto itotto 4096 112001:04 executable/
drwxr-xr-x 2 itotto itotto 4096 112001:04 source/

[itotto@itotto ]$ mkfifo named_pipe

[itotto@itotto ]$ \ls -l
合計 8
drwxr-xr-x 2 itotto itotto 4096 112001:04 executable/
prw-r--r-- 1 itotto itotto    0 112101:47 named_pipe|
drwxr-xr-x 2 itotto itotto 4096 112001:04 source/

[itotto@itotto ]$

3. システムコールを利用した方法

mknod()システムコールかもしくはmkfifo()関数を使用する。よく見たら上のコマンドと名前が一緒なので特に説明は要らなさそう。


/*** ノードを作成する ***/
// #include <unistd.h>
// #include <sys/types.h>
// #include <sys/stat.h>
// #include <fcntl.h>
// 
// 第一引数 pathname - ファイル名
// 第二引数 mode     - 作成するノードの設定値
// 第三引数 dev      - デバイスファイル以外は指定する必要なし
//
// 返却値
//  成功時 :  0
//  失敗時 : -1
int mknod(const char *pathname, mode_t mode, dev_t dev);

パイプ作成のサンプルは以下のとおりです。


      1 #include <unistd.h>
      2 #include <sys/stat.h>
      3 #include <stdio.h>
      4
      5 #define FILENAME    "/home/itotto/pg/c/named_pipe"
      6
      7 int main()
      8 {
      9     if (access(FILENAME, F_OK) == -1)
     10     {
     11         if (mknod(FILENAME, 0666 | S_IFIFO, 0) == -1)
     12         {
     13             perror("mknod()");
     14             return 1;
     15         }
     16         else
     17         {
     18             printf("パイプ(%s)を作成しました\n", FILENAME);
     19         }
     20     }
     21     else
     22     {
     23         printf("パイプ(%s)は既に存在します\n", FILENAME);
     24     }
     25
     26     return 0;
     27 }

説明し忘れていましたが、access()はファイルのpermissionを取得するためのシステムコールです。これがファイルの有無チェックにも使用できるようです。

# ↓定義はこんな感じ


/*** ファイルのpermissionチェック ***/
// #include <unistd.h>
// 
// 第一引数 pathname - ファイル名
// 第二引数 mode     - permissionのbit pattern
//
// 返却値
//  許可されている  :  0
//  許可されていない : -1
int access(const char *pathname, int mode);

ではプログラムを早速実行してみます。


[itotto@itotto ]$ \ls -l
合計 12
drwxr-xr-x 2 itotto itotto 4096 112001:04 executable/
-rw-r--r-- 1 itotto itotto  401 112102:19 mkmypipe.c
drwxr-xr-x 2 itotto itotto 4096 112001:04 source/

[itotto@itotto ]$ gcc -o mkmypipe mkmypipe.c

[itotto@itotto ]$ ./mkmypipe
パイプ(/home/itotto/pg/c/named_pipe)を作成しました

[itotto@itotto ]$ \ls -l
合計 20
drwxr-xr-x 2 itotto itotto 4096 112001:04 executable/
-rwxr-xr-x 1 itotto itotto 5300 112102:19 mkmypipe*
-rw-r--r-- 1 itotto itotto  401 112102:19 mkmypipe.c
prw-r--r-- 1 itotto itotto    0 112102:19 named_pipe|
drwxr-xr-x 2 itotto itotto 4096 112001:04 source/

[itotto@itotto ]$ ./mkmypipe
パイプ(/home/itotto/pg/c/named_pipe)は既に存在します

[itotto@itotto ]$

ファイルの有無チェックも含めて正常に動作している事が分かります。


続いてパイプの使用方法について。入力側と読み取り側で別のコンソールが必要になるので、二つの端末を立ち上げてください。


まず、パイプと言ってますが、一見普通のファイルですので試しにデータを書き込んで見ます。


[端末1]

[itotto@itotto ]$ \ls -l
合計 8
drwxr-xr-x 2 itotto itotto 4096 112113:21 executable/
prw-r--r-- 1 itotto itotto    0 112102:19 named_pipe|
drwxr-xr-x 2 itotto itotto 4096 112113:21 source/

[itotto@itotto ]$ echo I am itotto > named_pipe

↑ここで止まる

ファイルへ標準出力経由でデータをリダイレクトしたところで何も応答がなくなってしまいました。この状態でもう一つの端末を操作します。


[端末2]

[itotto@itotto ]$ \ls -l
合計 8
drwxr-xr-x 2 itotto itotto 4096 112113:21 executable/
prw-r--r-- 1 itotto itotto    0 112113:25 named_pipe|
drwxr-xr-x 2 itotto itotto 4096 112113:21 source/

[itotto@itotto ]$ cat named_pipe
I am itotto

[itotto@itotto ]$

named_pipeの中をcatで覗いてみると、先ほど[端末1]でリダイレクトしたデータを読み取る事が出来ます。ここでもう一度[端末1]へ戻ってみると先ほど止まっていた場所から先に進んでいる事がわかります。


[端末1]

[itotto@itotto ]$ echo I am itotto > named_pipe ←さっきはここで止まってた

[itotto@itotto ]$

名前付きパイプにデータを書き込んだ場合には、誰かにそれを受け取ってもらわない限り応答が返ってこなくなるようです。便利なような不便なような微妙な名前付きパイプ...。


次回はもっと便利に使いこなす方法をまとめます。


次へ進む

2012-02-18

Linuxシステムコールの勉強(その13)

Linuxシステムコール

Linuxシステムコール

前回はこちら


今回は親子に限定したプロセス間通信についてまとめます。まずは実際に親子間でパイプ経由でデータをやり取りするテストプログラムを作成します。

      1 #include <unistd.h>
      2 #include <stdio.h>
      3 #include <sys/types.h>
      4
      5 int main()
      6 {
      7     char buff[BUFSIZ];
      8     int  ppp[2];
      9       /*********************************************************************
     10        * ppp - 親プロセスへデータを受け渡すpipe用ファイルディスクリプタ配列
     11        *
     12        *   ppp[0]  file discriptor for reading
     13        *   ppp[1]  file discriptor for writing
     14        *********************************************************************/
     15
     16     int  ppc[2];
     17       /*********************************************************************
     18        * ppc - 子プロセスへデータを受け渡すpipe用ファイルディスクリプタ配列
     19        *
     20        *   ppc[0]  file discriptor for reading
     21        *   ppc[1]  file discriptor for writing
     22        *********************************************************************/
     23
     24     pid_t pid;
     25
     26     char *msg_p_to_c = "parent to child";
     27     char *msg_c_to_p = "child to parent";
     28
     29     /* pipeを開く */
     30     if (pipe(ppp) == -1 || pipe(ppc) == -1)
     31     {
     32         perror("pipe()");
     33         return 1;
     34     }
     35
     36     /* 子プロセス作成 */
     37     if ((pid = fork()) == -1)
     38     {
     39         perror("fork()");
     40         return 2;
     41     }
     42
     43     /* 親プロセスはここから */
     44     else if ( pid > 0 )
     45     {
     46         // 親プロセス→子プロセス パイプへのデータ書き込み //
     47         if (write(ppc[1], msg_p_to_c, strlen(msg_p_to_c) + 1) == -1 )
     48         {
     49             perror("write() from parent to child");
     50             close(ppc[1]);
     51             close(ppp[0]);
     52             return 1001;
     53         }
     54
     55         // 子プロセス→親プロセス パイプからデータ読み込み //
     56         if (read(ppp[0], buff, BUFSIZ) == -1 )
     57         {
     58             perror("read() from pipe");
     59             close(ppc[1]);
     60             close(ppp[0]);
     61             return 1002;
     62         }
     63         printf("i am parent process : [receive message] %s\n", buff);
     64         close(ppc[1]);
     65         close(ppp[0]);
     66     }
     67
     68     /* 子プロセスはここから */
     69     else
     70     {
     71         // 子プロセス→親プロセス パイプへのデータ書き込み //
     72         if (write(ppp[1], msg_c_to_p, strlen(msg_c_to_p) + 1) == -1 )
     73         {
     74             perror("write() from child to parent");
     75             close(ppc[0]);
     76             close(ppp[1]);
     77             return 2001;
     78         }
     79
     80         // 親プロセス→子プロセス パイプからデータ読み込み //
     81         if (read(ppc[0], buff, BUFSIZ) == -1 )
     82         {
     83             perror("read() from pipe");
     84             close(ppc[0]);
     85             close(ppp[1]);
     86             return 2002;
     87         }
     88         printf("i am child process : [receive message] %s\n", buff);
     89         close(ppc[0]);
     90         close(ppp[1]);
     91     }
     92
     93     return 0;
     94 }

2点、本が誤っている箇所があるので追記。

(1) 親と子で処理が分かれたところで使用しないパイプを閉じていましたが閉じちゃダメ

  → ファイルディスクリプタは共有なので一方を閉じると使えなくなる

(2) sys/types.hもincludeする

  → pid_tが定義されている

で、これを実行してみます

[itotto@itotto ]$ gcc -o fam_pipe fam_pipe.c

[itotto@itotto ]$ ./fam_pipe
i am parent process : [receive message] child to parent
i am child process : [receive message] parent to child

[itotto@itotto ]$

互いに送信したメッセージを受信している事が分かります。パイプを2つ開いて互いに書き込んで互いに読み込んでいるわけですから、当然です。


さて。ここまでは大丈夫なのですが、例えばfork()で作成した子プロセスがexec()した場合を考えて見ます。単にfork()しただけであれば、fork()前に開いておいたpipeのファイルディスクリプタが利用できますが、一旦exec()してしまうと全くの別プロセスになってしまうのでこれを利用する事が出来なくなってしまいます。


この本では


    1. exec()するプログラムに引数でディスクリプタを渡す方法
    2. パイプのディスクリプタを標準入出力にリダイレクトする方法

を紹介しています。前者の「引数でディスクリプタを渡す方法」は全然面白くないので、ここでは後者を説明します。

      1 #include <unistd.h>
      2 #include <stdio.h>
      3 #include <sys/types.h>
      4
      5 int main()
      6 {
      7     int pipe_fd[2];
      8     pid_t pid ;
      9
     10     /* pipeを開く */
     11     if (pipe(pipe_fd) == -1)
     12     {
     13         perror("pipe");
     14         return 1;
     15     }
     16
     17     /* 自プロセスを子プロセスに複写 */
     18     if ((pid = fork()) == -1)
     19     {
     20         perror("fork()");
     21         return 2;
     22     }
     23
     24     /* 以下親プロセス専用の処理 */
     25     else if ( pid > 0 )
     26     {
     27         // 標準入力を閉じる //
     28         close(fileno(stdin));
     29
     30         // 標準入力にパイプをリダイレクト //
     31         dup(pipe_fd[0]);
     32         close(pipe_fd[0]);
     33
     34         close(pipe_fd[1]);
     35
     36         if (execlp("less", "less", "-r", NULL) == -1)
     37         {
     38             perror("execlp()");
     39             return 1001;
     40         }
     41
     42     }
     43     /* 親プロセス専用ここまで */
     44
     45     /* 以下子プロセスのターン */
     46     else
     47     {
     48         // 標準出力を閉じる //
     49         close(fileno(stdout));
     50
     51         // 標準出力にパイプをリダイレクト //
     52         dup(pipe_fd[1]);
     53         close(pipe_fd[1]);
     54
     55         close(pipe_fd[0]);
     56
     57         if (execlp("ls", "ls", "--color", "-C", NULL) == -1)
     58         {
     59             perror("execlp()");
     60             return 2001;
     61         }
     62
     63     }
     64     /* 子プロセス専用ここまで */
     65
     66     return 0;
     67 }

fork()で親子それぞれの処理に分かれた後に、(1) 親プロセス側でパイプ読取用のファイルディスクリプタを標準入力へリダイレクトし、(2) 子プロセス側でパイプ書込用のファイルディスクリプタを標準出力へリダイレクトさせてます。

こうする事で書き込む方は標準出力に出力し、読み取る方は標準入力を読み取ることでファイルディスクリプタが不明のままでもプロセス間の通信をすることが可能となります。


[itotto@itotto ]$ gcc -o pipe_via_stdio pipe_via_stdio.c
[itotto@itotto ]$ ./pipe_via_stdio

executable  pipe_via_stdio  pipe_via_stdio.c  source

[itotto@itotto ]$

というわけで匿名パイプはここまで。想像してたのよりも簡単で分かりやすい印象です。

次へ進む

2012-02-17

Linuxシステムコールの勉強(その12)

Linuxシステムコール

Linuxシステムコール

前回はこちら


今回からプロセス間通信の方法として利用されるパイプについてまとめます。


まずパイプとは何かというと、「メモリ内に設けられたバッファ領域をプロセス間通信の経路として利用出来るようにしたもの」となります。つまり、プロセスとプロセスの間でデータをやり取りする場合に使用出来るのがパイプなのです。パイプと言えばシェルの機能としても実装されているあのパイプを思い出す人も多いでしょうが、実行したコマンドの結果をlessとかmoreとかgrepに渡すために使っているあのパイプはまさしくこのパイプのことです。


パイプの動作イメージをちょっと図解してみました。


f:id:itotto:20120215221942p:image


ここでパイプについて注目したいのは、データをやり取りするプロセスの組み合わせが豊富であるという事です。


    1. 一つのプロセス内での通信処理
    2. 同一サーバ or 端末内での別プロセス間の通信処理
    3. 互いに異なる端末間でのネットワーク経由での通信処理

パイプはこのようなさまざまな通信処理に利用することが出来ます。

ここまでパイプ、パイプと一言で書いてきましたが、パイプには大きく二つの種類があります。


名前システムコール同一プロセス間通信別プロセス間通信ネットワーク経由通信
名前なしパイプ
(匿名パイプ)
pipe()
(親子プロセス間でのみ可)
×
名前付きパイプmdnod()

まずは匿名パイプから説明していきます。

上述のとおり、匿名パイプはpipe()というシステムコールを使用することで使うことが出来ます。


/*** パイプを開く ***/
// #include <unistd.h>
// 
// 第一引数 パイプを読み書きするためのファイルディスクリプタを取得
//
// 返却値
//  処理成功 :  0
//  処理失敗 : -1
int pipe(int fd[2]);

pipe()で開いたパイプに対してデータの読み書きをする場合は、ファイル同様read/writeを使用する。その際に使用するファイルディスクリプタは↑の引数を使用する。

また、読み書きに限らず後始末もファイルと同様に必須処理です。後始末にはclose()を使用します。


まずは標準入力から入力されたデータをパイプへ書き込み、そのパイプからデータを読み取って標準出力へ表示するプログラムを作成してみます。


      1 #include <unistd.h>
      2 #include <stdio.h>
      3 #include <string.h>
      4
      5 int main()
      6 {
      7     char buff[BUFSIZ];
      8     int  pipe_ds[2]  ;
      9
     10     /*** pipeを開く ***/
     11     if (pipe(pipe_ds) == -1)
     12     {
     13         perror("pipe");
     14         return 1;
     15     }
     16
     17     while (fgets(buff, BUFSIZ, stdin))
     18     {
     19         /*** パイプにデータを書き込み ***/
     20         if (write(pipe_ds[1], buff, strlen(buff) + 1) == -1)
     21         {
     22             perror("write");
     23
     24             /*** 後片付け ***/
     25             close(pipe_ds[0]);
     26             close(pipe_ds[1]);
     27             return 2;
     28         }
     29
     30         /*** パイプからデータを読み込む ***/
     31         if (read(pipe_ds[0], buff, BUFSIZ) == -1)
     32         {
     33             perror("read");
     34
     35             /*** 後片付け ***/
     36             close(pipe_ds[0]);
     37             close(pipe_ds[1]);
     38             return 3;
     39         }
     40
     41         printf("message from  pipe : %s\n", buff);
     42     }
     43
     44     /*** 後片付け ***/
     45     close(pipe_ds[0]);
     46     close(pipe_ds[1]);
     47 }

注意点は、一度オープンしたパイプはファイル同様必ずクローズするというところです。さて、さっそく実行してみます。


[itotto@itotto ]$ gcc -o anony_pipe1 anony_pipe1.c
[itotto@itotto ]$ ./anony_pipe1
vine linux
message from  pipe : vine linux

hogehoge
message from  pipe : hogehoge

[itotto@itotto ]$ ls -l | ./anony_pipe1
message from  pipe : 合計 24

message from  pipe : -rwxr-xr-x 1 itotto itotto 5809 111500:51 anony_pipe1*

message from  pipe : -rw-r--r-- 1 itotto itotto  743 111500:51 anony_pipe1.c

message from  pipe : drwxr-xr-x 2 itotto itotto 4096 111422:17 executable/

message from  pipe : drwxr-xr-x 2 itotto itotto 4096 111422:17 source/

[itotto@itotto ]$

手始めにプログラムを単独で起動してみました。fgets()が呼び出されているので標準入力からの入力待ち受け状態でウェイトします。そこで"vine linux"と入力したところ、"message from pipe : vine linux"と表示されました。これは標準入力から入力されたデータがパイプに書き込まれ、そのパイプからデータを読み取った後に41行目にprintf()で標準出力に出力をしている部分がこの処理を担当しています。


続いて、ls -lというありきたりのコマンドをパイプ経由(標準入力)でプログラムに渡したところ、手で入力する代わりにls -lの結果がプログラムに引き渡されました。うまく動いているようです。


次回はfork()を使用して親子間でプロセス通信をする方法、そしてexec()を使用した場合の問題点についてまとめます。


次へ進む

2012-02-16

Linuxシステムコールの勉強(その11)

Linuxシステムコール

Linuxシステムコール

前回はこちら


今回は、RAWモードにおける制御文字であるVMINとVTIMEについて説明します。ioctl()を使った端末制御については今回が最後です。


まずはVMINを変更するプログラムのソースとその動作を見て見ます。

      1 #include <sys/ioctl.h>
      2 #include <stdio.h>
      3 #include <asm/termbits.h>
      4
      5 int main()
      6 {
      7     char c[BUFSIZ];
      8     struct termio tm, tm_save;
      9     int  inputNum;
     10     int  fd_stdin, fd_stdout;
     11
     12     fd_stdin  = fileno(stdin);
     13     fd_stdout = fileno(stdout);
     14
     15     /* 現在の設定を格納 */
     16     ioctl(fd_stdin, TCGETA, &tm);
     17     tm_save = tm;
     18
     19     /* 入力したい文字数を取得 */
     20     printf("何文字入力しますか?");
     21     fscanf(stdin,"%d", &inputNum);
     22
     23     /* 端末に設定を反映 */
     24     tm.c_lflag &= ~ICANON;
     25     tm.c_cc[VMIN] = inputNum;
     26     ioctl(fd_stdin, TCSETAF, &tm);
     27
     28     printf("%d文字入力してください?", inputNum);
     29     fflush(stdout);
     30     read(fd_stdin, &c, inputNum);
     31
     32     // 整形用 //
     33     write(fd_stdout, "\n", 1);
     34
     35     write(fd_stdout, c, inputNum);
     36
     37     // 整形用 //
     38     write(fd_stdout, "\n", 1);
     39
     40     /* 設定を元に戻す */
     41     ioctl(fd_stdin, TCSETAF, &tm_save);
     42
     43     return 0;
     44 }

24行目でRAWモードへ移行し、25行目でVMINの値を標準入力からの入力値に変更しています。ではこれを動かしてみましょう。


[itotto@itotto ]$ gcc -o change_vmin change_vmin.c

[itotto@itotto ]$ ./change_vmin
何文字入力しますか?5
5文字入力してください?12345
12345

[itotto@itotto ]$ ./change_vmin
何文字入力しますか?10
5文字入力してください?1234567890
1234567890

[itotto@itotto ]$

入力文字数として"5"を入力した場合にはVMINの値は5になっています。この際、read()が動作するのは5文字のデータ入力が完了したタイミングです。同様に入力文字数として"10"を入力した場合には10文字入力したタイミングでread()が動作しています。


つまりVMINの値はバッファに何バイトのデータが入ったらread()を呼び出すかという事を決めるための値である事が分かります。


続いてVTIMEの値について調べるために以下のプログラムを使用します。

      1 #include <sys/ioctl.h>
      2 #include <stdio.h>
      3 #include <asm/termbits.h>
      4
      5 int main()
      6 {
      7     char c;
      8     struct termio tm, tm_save;
      9     int  fd_stdin;
     10     int  n, result;
     11
     12     fd_stdin  = fileno(stdin);
     13
     14     /* 現在の設定を格納 */
     15     ioctl(fd_stdin, TCGETA, &tm);
     16     tm_save = tm;
     17
     18     /* 端末に設定を反映 */
     19     tm.c_lflag    &= ~(ECHO | ICANON);
     20     tm.c_cc[VMIN]  = 0;
     21     tm.c_cc[VTIME] = 1;
     22     ioctl(fd_stdin, TCSETAF, &tm);
     23
     24     result = 0;
     25
     26     printf("終了する場合はCtrl + x\n");
     27
     28     while(1)
     29     {
     30         // キー入力なし //
     31         if ((n = read(fd_stdin, &c, 1)) == 0)
     32         {
     33             printf("入力文字なし\n");
     34         }
     35
     36         // システムコール(read())でエラー //
     37         else if (n == -1)
     38         {
     39             perror("read");
     40             result = 1;
     41             break ;
     42         }
     43
     44         // 入力文字がctrl+xの場合 //
     45         else if (c == '\x18')
     46         {
     47             result = 0;
     48             break;
     49         }
     50         else
     51             printf("%c\n", c);
     52     }
     53
     54     /* 設定を元に戻す */
     55     ioctl(fd_stdin, TCSETAF, &tm_save);
     56
     57     return result;
     58 }

19行目でRAWモードへ移行し、20行目でVMINの値を0に、21行目でVTIMEの値を変更しています。VMINが0という事は入力を受け付けるバッファの単位が0なので、即時読み取りを意味しています。

ではこれもためしに動かしてみます。


[itotto@itotto ]$ gcc -o ts_get_keycode ts_get_keycode.c

[itotto@itotto ]$ ./ts_get_keycode
終了する場合はCtrl + x
入力文字なし
a
入力文字なし
入力文字なし
v
入力文字なし
入力文字なし
入力文字なし
b
v
入力文字なし
入力文字なし

[itotto@itotto ]$

このとおり、キー入力が無ければ「入力文字なし」と表示されますが、何かキーを押すとその文字が表示されます。


実際に試してみると分かりますが、VTIMEを0にした場合と1にした場合では、「入力文字なし」と表示されるタイミング異なります。0にした場合はフォローのしようのないくらい、あっという間に画面表示が流されます。対して1に設定した場合には適度な間隔で表示が更新されます。

これより、VTIMEはread()を実行した際に、バッファからデータの受け渡しが無い場合に待つ時間をあらわすものであることがわかります。ちなみに単位は1/10秒です。


で。

このVMINとVTIMEを使ってESCキーとファンクションキーの区別が出来たりするよという事がこの本には書いてありますが、あまり面白くないので割愛します。また必要になったら読み返そう。


最後にキーコードを調べるプログラム(xevのCUI版)をサンプルとして作成して動作させてみます。


      1 #include <sys/ioctl.h>
      2 #include <stdio.h>
      3 #include <asm/termbits.h>
      4
      5 int main()
      6 {
      7     char c;
      8     struct termio tm, tm_save;
      9     int  fd_stdin;
     10     int  n, result;
     11
     12     fd_stdin  = fileno(stdin);
     13
     14     /* 現在の設定を格納 */
     15     ioctl(fd_stdin, TCGETA, &tm);
     16     tm_save = tm;
     17
     18     /* 端末に設定を反映 */
     19     tm.c_lflag    &= ~(ECHO | ECHONL |ICANON);
     20     tm.c_cc[VMIN]  = 1;
     21     tm.c_cc[VTIME] = 0;
     22
     23     tm.c_iflag &= ~ICRNL ; // \r -> \n変換をしない //
     24     tm.c_oflag &= ~ONLCR ; // \n -> \r変換をしない //
     25     tm.c_lflag &= ~ISIG  ; // シグナルを無視する   //
     26
     27     tm.c_iflag &= ~IXOFF ; // c-s, c-qの制御(入力) //
     28     tm.c_iflag &= ~IXON  ; // c-s, c-qの制御(出力) //
     29
     30     ioctl(fd_stdin, TCSETAF, &tm);
     31
     32     result = 0;
     33
     34     printf("終了する場合はCtrl + x\n");
     35
     36     while(1)
     37     {
     38         // キー入力なし //
     39         if (read(fd_stdin, &c, 1) == -1)
     40         {
     41             perror("read");
     42             result = 1;
     43             break ;
     44         }
     45
     46         // 入力文字がctrl+xの場合 //
     47         else if (c == '\x18')
     48         {
     49             result = 0;
     50             break;
     51         }
     52         else
     53             printf("%02x : %c\r\n",c ,c);
     54     }
     55
     56     /* 設定を元に戻す */
     57     ioctl(fd_stdin, TCSETAF, &tm_save);
     58
     59     return result;
     60 }

[itotto@itotto ]$ gcc -o get_keycode get_keycode.c

[itotto@itotto ]$ ./get_keycode
終了する場合はCtrl + x
61 : a
62 : b
63 : c
64 : d
21 : !
48 : H
5e : ^
5d : ]

[itotto@itotto ]$

次回からプロセス間通信(名前付きパイプ)に入ります。端末長かった...。


次へ進む