LoginSignup
25
28

More than 3 years have passed since last update.

[C言語] Head First Cを読んだメモ

Last updated at Posted at 2014-05-02

やはり基本は大切だろうと、改めてHead First Cというオライリーの本を読んでいます。
前半は復習的な感じでしたが、後半は知っているもののしっかりと調べたことがなかったのでとても面白く読みました。

がっつりCでなにかを書く、ということはあまりないかもしれませんが、Objective-Cを触っている以上どこかで必ず出てくると思うので、読んだ感想とメモを書いておこうと思います。


子プロセス

子プロセスは親プロセスが生成した新しいプロセスです。
子プロセスの生成にはfork()関数を使います。

child-process.c
// 子プロセスは`fork()`システムサービスを用いる

#include <stdio.h>
#include <unistd.h> // exec系システムサービスを使う場合に読み込む
#include <errno.h>  // errnoを列挙したヘッダファイル
#include <string.h> // errnoを文字列にして分かりやすくしてくれる関数がある


int main()
{
    for (int i = 0; i < 3; i++) {
        // 子プロセスを生成
        pid_t pid = fork();

        // pidが`-1`の場合は子プロセス失敗
        if (pid == -1) {
            fprintf(stderr, "子プロセスをフォーク出来ません - %s\n", strerror(errno));
        }

        // pid == 0 の場合は子プロセス。(親プロセスの場合は0以外の整数)
        if (!pid) {
            if (execl("/sbin/ifconfig", "/sbin/ifconfig", NULL) == -1) {
                if (execlp("ipconfig", "ipconfig", NULL) == -1) {
                    fprintf(stderr, "ipconfigを実行できません - %s", strerror(errno));
                    return 1;
                }
            }
        }
    }

    return 0;
}

pid_tという型はint相当ですが、OSによってはshortなど異なる場合があります。
そうした場合でも適切に利用できるように、専用の型が用意されています。


プロセス間通信

ファイルディスクリプタ

ファイルディスクリプタとは、データストリーム(データの流れ)を表す数値のことです。
通常は0が標準入力、1が標準出力、2が標準エラーです。
コマンドラインで以下のようにすると処理がリダイレクトできていました。

$ ifconfig > some.txt 2> error.log

この2がまさに標準エラーのストリーム番号です。

ファイルディスクリプタテーブル

OSはどのデータストリームがどこに接続されているか、というのを管理するのに ファイルディスクリプタテーブル というテーブルを使って管理します。

とある状態のディスクリプタテーブルの例です。

番号 データストリーム
0 キーボード
1 画面
2 画面
3 データベース接続

標準入力はキーボード、標準出力とエラーは画面、そして新規のストリームがデータベースを指している、という状態です。

ファイルディスクリプタという名前ですが、必ずしもファイルに接続しているわけではありません。
また、以下のようにファイルを開くと新しくファイルディスクリプタテーブルが更新されます。

sample.c
FILE *file = fopen("sample.txt", "w");

// fileno(file)でファイルディスクリプタ番号を取得できる
番号 データストリーム
0 キーボード
1 画面
2 画面
3 データベース接続
4 ファイル「sample.txt」

ファイルの処理はこうした「データの流れをどこに接続するか」ということを意味しています。

dup2()関数は、ファイルディスクリプタテーブルを書き換える

上記のファイルディスクリプタテーブルの番号を入れ替えると色々な処理をリダイレクトすることができます。
(例えば読み込み用に開いたファイルからデータを読み取り、データベースに流す、など)

通常は標準出力は画面につながっているので結果などが画面に表示されるわけです。
これをファイルに接続しなおすとファイルに書き出し、という意味になります。

この接続先をプログラムから変更するのがdup2()関数です。
第一引数のファイルディスクリプタを、第二引数のファイルディスクリプタとして 複製(上書き) する関数です。
つまり、第二引数が指していたファイルディスクリプタは、第一引数のファイルディスクリプタが指していた番号で上書きされます。

sample.c
dup2(fileno(file), 1);

のように実行すると、テーブルは以下のようになります。

番号 データストリーム
0 キーボード
1 画面 ファイル「sample.txt」
2 画面
3 データベース接続
4 ファイル「sample.txt」

以降、標準出力(番号1)宛に来たデータは、ファイル「sample.txt」に書き込まれるようになります。


子プロセスの終了を待つ

場合によっては子プロセスの実行を待ってからでないと処理が失敗する場合があります。
例えば、子プロセスでなにがしかの処理を行い、それをファイルに出力するとします。
この処理が長い場合、子プロセスを生成してすぐに該当ファイルを読み込んでも、まだ処理が終わっていないために該当ファイルは空のままです。

こうしたことから、子プロセスの処理が終了するのを待つ必要がある場合があります。
その際に使うのがwaitpid関数です。

waitpid-sample.c
#include <sys/wait.h> // waitpidのために必要

int pid_status;

if (waitpid(pid, &pid_status, 0) == -1) {
    fprintf(stderr, "子プロセス待機エラー");
    return 1;
}

waitpid関数は引数にpidとステータス用のint型の変数を取ります。
ステータスは随時変わっていくので、参照形式で渡します。


ふたつのプロセスをつなぐpipe()関数

上記では子プロセスの終了を待ってから処理をするようにしましたが、そもそもプロセス間でなにかしらやりとりがしたい場合があります。
その際に利用するのがpipe()関数です。

コマンドラインの「パイプ」と同じ役割をコードから利用するものです。

pipe-sample.c
int main()
{
    // パイプ用ストリーム(ファイルディスクリプタ)
    // fd[0]は読み込み用、fd[1]は書き込み用として割り当てられる
    int fd[2];
    if (pipe(fd) == -1) {
        fprintf(stderr, "パイプを作成できません");
        return 1;
    }

    pid_t pid = fork();
    if (!pid) {
        close(fd[0]);   // 子プロセスはパイプから読み込まないので閉じる
        dup2(fd[1], 1); // 標準出力をパイプに変更する
    }

    close(fd[1]);   // 親プロセスは書き込まないので閉じる
    dup2(fd[0], 0); // 標準入力を、子プロセスからの入力に切り替える

    return 0;
}

プロセスの死

シグナルの種類

シグナル名 意味
SIGINT プロセスが割り込まれた
SIGQUIT 誰かがプロセスに停止してコアダンプファイルにメモリをダンプするように要求した
SIGFPE 浮動小数点エラー
SIGTRAP デバッガがプロセスの実行箇所を尋ねた
SIGSEGV プロセスが不正なメモリにアクセスしようとした
SIGWINCH 端末ウィンドウのサイズが変更された
SIGTERM 誰かがカーネルにプロセスを終了するように要求した
SIGPIPE プロセスが誰も読み込んでいないパイプに書き込んだ

raise()関数を使ってシグナルを送る

シグナルを受け取った際、さらに別のシグナルを自分自身に送りたい場合があります。
その際に利用するのがraise()関数です。
raise(SIGTERM);などのようにシグナルを送信します。


ソケットとネットワーキング

ソケット周りはしっかり勉強しておきたかったので、一番読みたい章でしたw
基本的な概念はデータストリームと似ていて、双方向でやりとりができるデータの流れです。
とあるソケットにデータを流せば、接続されている反対側に届く、と。

本にあったサンプルに補足を付けつつ、ソケットを実現するコードサンプルを書いておきます。

socket-sample.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>


// エラー表示関数
void error(char *msg)
{
    fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(1);
}

// ソケットを開く
int open_listener_socket()
{
    int s = socket(PF_INET, SOCK_STREAM, 0); // PFはProtocol Familyの頭文字
    if (s == -1) {
        error("ソケットを開けません");
    }
    return s;
}


// ソケットをポートに関連付けるヘルパー関数
void bind_to_port(int socket, int port)
{
    struct sockaddr_in name;
    name.sin_family = PF_INET;
    name.sin_port   = (in_port_t)htons(port);
    name.sin_addr.s_addr = htonl(INADDR_ANY);

    int reuse = 1;
    if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) {
        error("ソケットに再利用オプションを設定できません");
    }

    int c = bind(socket, (struct sockaddr *)&name, sizeof(name));
    if (c == -1) {
        error("ソケットにバインドできません");
    }
}


// ソケットにデータを送る
int say(int socket, char *s)
{
    int result = send(socket, s, strlen(s), 0);
    if (result == -1) {
        fprintf(stderr, "%s: %s\n", "クライアントとの通信エラー", strerror(errno));
    }
    return result;
}


// シグナルハンドラの設定用ヘルパー関数
int catch_singal(int sig, void (*handler)(int))
{
    struct sigaction action;
    action.sa_handelr = handler;
    sigemptyset(&action.sa_mask);
    action.sa_flags = 0;
    return sigaction(sig, &action, NULL);
}


// シグナルを受け取った時の処理
int listener_d;
void handler_shutdown(int sig)
{
    if (listener_d) {
        close(listener_d);
        fprintf(stderr, "Goodbye!!\n");
        exit(0);
    }
}

// ソケットからの情報を読み取る
int read_in(int socket, char *buf, int len)
{
    char *s = buf;
    int slen = len;
    int c = recv(socket, s, slen, 0);
    while((c > 0) && (s[c - 1] != '\n')) {
        s += c;
        slen -= c;
        c = recv(socket, s, slen, 0);
    }

    if (c < 0) {
        return c;
    }
    else if (c == 0) {
        buf[0] = '\0';
    }
    else {
        s[c - 1] = '\0';
    }

    return len - slen;
}


// main関数
int main(int argc, char *argv[])
{
    if (catch_signal(SIGINT, handle_shutdown) == -1) {
        error("割り込みハンドラを設定できません");
    }

    listener_d = open_listener_socket();
    bind_to_port(listener_d, 30000);

    if (listen(listener_d, 10) == -1) {
        error("接続待ちできませんでした");
    }

    struct sockaddr_storage client_addr;
    unsigned int address_size = sizeof(client_addr);

    puts("接続を待っています");

    while(1) {
        int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);

        if (connect_d == -1) {
            error("第二のソケットを開けません");
        }

        if (!fork()) {

            // 子プロセスならメインのリスナソケットを閉じる
            close(listener_d);

            if (say(connect_d, "インターネットノックノックプロトコルサーバ\r\nバージョン1.0\r\nKnock! Knock!") != -1) {
                read_in(connect_d, buf, sizeof(buf));

                if (strncasecmp("Who's there?", buf, 12)) {
                    say(connect_d, "「Who's there?」と入力する必要があります");
                }
                else {
                    // 他に手順や処理が必要なら書く
                }
            }

            // 子プロセスの処理が終了したらプロセスを終了する
            close(connect_d);
            exit(0);
        }

        // 親プロセスは子プロセス用のソケットを閉じる
        close(connect_d);
    }

    return 0;
}

サーバサイドのソケット通信手順

ソケット通信のサーバサイドの手順はBLABです。以下の手順の頭文字です。

  • ポートにバインドする(Bind)
  • 接続待ちする(Listen)
  • 接続を受け入れる(Accept)
  • やり取りを開始する(Begin)

クライアントサイドのソケット通信手順

  • リモートポートに接続
  • やり取りの開始

getaddrinfo()はホストのアドレスを取得する

メモ

ちょいちょい出てくる略の意味とか調べたのでメモ。

  • htons ... Host to Network Shortの略
  • htonl ... Host to Network Longの略
  • PF_INET ... PFはProtocol Familyの略

send() / recv()関数

ソケットディスクリプタを用いてデータの送受信を行う。
send()とrecv()は対の関係。

[プロトタイプ]

send-recv.c
int send(int socket, const void *msg, unsigned int msgLength, int flag);
int recv(int socket, void *msg, unsigned int msgLength, int flag);

[引数]

引数名 意味
socket ソケットディスクリプタを指定する
msg 送受信するメッセージ領域を指定する
msgLength メッセージの文字列長を指定する
flag ソケットが呼び出された際のデフォルト動作を変更する。flagに「0」を指定するとデフォルト動作となる

[戻り値]
正常時は送受信したバイト数が返る。(エラーの場合は-1が返る)


スレッド

いわゆるマルチスレッドの話です。
マルチスレッドを実現するためのライブラリとして、pthreadライブラリを利用します。

マルチスレッドを実行している部分を抜き出すとこんな感じ。

thread-sample.c
int main()
{
    pthread_t t0;
    pthread_t t1;

    if (pthread_create(&t0, NULL, dose_not, NULL) == -1) {
        error("スレッドt0を作成できません");
    }

    if (pthread_create(&t1, NULL, dose_too, NULL) == -1) {
        error("スレッドt1を作成できません");
    }

    void *result;
    if (pthread_join(t0, &result) == -1) {
        error("スレッドt0をジョインできません");
    }
    if (pthread_join(t1, &result) == -1) {
        error("スレッドt1をジョインできません");
    }
}

pthread_t型の構造体にスレッド情報を格納します。
pthread_create()関数でスレッドを作成し、pthread_join()関数ですべてのスレッドが終了するのを待ちます。

スレッドセーフ

マルチスレッドで制御が難しいのがスレッドセーフな処理です。
エラーにはならないので、バグが発生した際に箇所を特定するのが大変な部分でしょう。
アプリ制作ではたまにしか発生しない、みたいなことはよくあるのでより難しくする要因です。

pthreadライブラリにはスレッドセーフの仕組みを簡単に導入する方法が準備されています。
MUTEX と呼ばれる仕組みです。
※MUT-EX(ミューテックス)... 相互に(MUTually)排他的(EXclusive)

利用するにはpthread_mutex_t型変数に適切な情報を格納し、pthread_mutex_lock() / pthread_mutex_unlock()関数を呼び出します。

mutex-sample.c
pthread_mutex_t lock_name = PTHREAD_MUTEX_INITIALIZER; // マクロを利用してセットアップ

// スレッドセーフにしたい処理をしたいところで下記を記述
pthread_mutex_lock(&lock_name);

// do something.

pthread_mutex_unlock(&lock_name);

読んだ所感

読んでみて、だいぶ幅広い書籍だなーと思いました。
最初はそれこそ「変数とは」みたいなところから入り、最後はマルチスレッドの話で終わります。
ページ数は600近くあるのでだいぶありますが、網羅的に知りたい人はいい書籍ではないでしょうか。
Objective-Cをやる上で、Cは避けて通れないのでだいぶ役に立つ知識が手に入りました。


参考記事

25
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
28