JUNのブログ

JUNのブログ

活動記録や技術メモ

C言語でファイル(ファイルディスクリプタ)から1行ずつ取得する関数を作った

10月から42Tokyoで学んでいるのですが, そこでやった課題の内容及びコードの公開がOKってことなんで, これから気が向いたら42の課題と自分の解答を記事にしていこうと思います. (ただし, 課題PDF自体は再配布や外部への公開が禁止されているので, 課題内容はあくまで自分が要約したものであることをご了承頂きたい)

今回の課題に対する自分の解答のソースコードは以下のリポジトリにうpしてある.

github.com

まえがき (for 42 students)

42の学生で get_next_line の課題が終了していない方は今すぐブラウザバックすることを強くおすすめします.

本記事には実装レベルでの説明が多く含まれており, 盛大なネタバレを食らうことになります.

課題内容

渡されたファイルディスクリプタ(以降 fd と呼ぶ)からデータを読み込み, 関数を呼び出す度に1行ずつ(改行文字は除く)渡されたバッファー変数に格納し, 処理結果(正常=1, 異常=-1 or 終了=0)を返す関数を作成する.

読み込む際には事前に #define された BUFFER_SIZE という定数分を一度に読み込む必要がある. read(fd, &buf, BUFFER_SIZE);

プロトタイプ宣言は以下のようにすること.

int get_next_line(int fd, char **line);

課題の意図としては以下の2つがメインみたいです.

  • 行ごとに読み込むという汎用性の高い関数を実装する
  • C言語のstatic変数について理解する

制約

今回使用可能な標準Cライブラリで定義されている関数は以下の通りです.

また, 42にはNorminetteというLinterプログラムがあり, Linterプログラムがエラーを吐いた場合, 課題は問答無用で0点となる.

Norminetteが課す制約はいくつもあり, すべて列挙するのは大変なのだが, 大きな制約としては以下の通りである.

  • 各行は最大80文字
  • 各関数には最大25行まで
  • 各関数内で定義出来る変数は5つまで
  • 各関数内で受け取れる名前付きパラメータ4つまで
  • 1行につき1命令 (例えばint a= 0は宣言と代入を同時に行っているので不可)
  • 列挙型には e_プレフィックスを付けるなどのいくつかの命名規則
  • 以下の命令の使用禁止
    • for
    • do..while
    • switch
    • case
    • goto

これ以外にも複数の制約があり, もし自分のコード内で 「なんでこんな面倒くさい書き方してるんだ?」って思うことがあったら恐らくそれはNorminetteのエラーを消すためにそのように書いてる場合が多い. (そうじゃないときもあるので遠慮なくコメントください)

また, 本来はNorminette違反ですが, ブログのためにコメントを複数追加しています.

提出ファイル

  • get_next_line.c
  • get_next_line_utils.c
  • get_next_line.h

この3つのファイルのみを提出すること.

実装

get_next_line.h

# include <stdlib.h>
# include <unistd.h>

# define SUCCESS       1
# define END_OF_FILE   0
# define ERROR         -1
# define CONTINUE_READ -2

# ifndef BUFFER_SIZE
#  define BUFFER_SIZE 256
# endif

int        get_next_line(int fd, char **line);
char   *ft_strdup(const char *s);
char   *ft_substr(char const *s, unsigned int start, size_t len);
size_t ft_strlen(const char *s);
char   *ft_strchr(const char *s, int c);
char   *ft_strjoin(char const *s1, char const *s2);

基本的には定数の定義と get_next_line_utils.cの関数のプロトタイプ宣言入っているだけ.

get_next_line.c

get_next_line.c では以下の関数を定義している

int get_next_line(int fd, char **line);
static int read_process(int fd, char **line, char **save);
static int join_line_and_save(char **line, char **save);
static int    join_line_and_buf(char **line, char *buf);
static int split_by_newline(char **line, char **save, char *buf);

int get_next_line(int fd, char **line);

get_next_line.c 内で定義している他の関数を呼び出して動作する. 読み込んだ文字列でlineに結合しなかった残りの文字列を char*型のstatic変数 save で次回関数が呼ばれた時のために保管している.

int            get_next_line(int fd, char **line)
{
    int            ret;
    static char   *save;

    if (fd < 0 || !line || BUFFER_SIZE <= 0 || !(*line = ft_strdup("")))
        return (ERROR);
    ret = CONTINUE_READ;
    if (save)
        ret = join_line_and_save(line, &save);
    if (ret == CONTINUE_READ)
        ret = read_process(fd, line, &save);
    return (ret);
}

やっていることとしては

  • char*型のstatic変数 save を宣言
  • 以下の条件の時は問答無用でエラーとして返す
    • fd が負の値
    • line がNULL
    • BUFFER_SIZE が0以下
    • line の初期化処理に失敗 (!(*line = ft_strdup("")) の部分)
  • もし前回読み込んだ文字列が残っていたら join_line_and_save(line, &save) を呼び出して前回読み込んだ残りの文字列を line に格納する. もしsave内に改行が入っていた場合, ret には SUCCESS が入る. 改行が入っていない場合は CONTINUE_READ が入る.
  • 前回読み込んだ文字列の残りの中に改行がない場合はfdから新たに読み込む.

といった感じです.

基本的にはバリデーションと前回読み込んだ文字列の保持などを行っています. 実際にfdから読み込んだり, 文字列から改行を検出して文字列を分割するのは他の関数の仕事です.

static int join_line_and_save(char **line, char **save);

前回 get_next_line() が呼び出された時に保存した saveline に何文字コピーするかを計算し, コピーする.

static int join_line_and_save(char **line, char **save)
{
    char   *tmp;
    char   *newline_ptr;

    if ((newline_ptr = ft_strchr(*save, '\n')))
    {
        tmp = *line;
        *line = ft_substr(*save, 0, newline_ptr - *save);
        free(tmp);
        if (!(*line))
            return (ERROR);
        tmp = *save;
        *save = ft_substr(newline_ptr + 1, 0, ft_strlen(newline_ptr + 1));
        free(tmp);
        if (!(*save))
            return (ERROR);
        return (SUCCESS);
    }
    else
    {
        tmp = *line;
        *line = *save;
        *save = NULL;
        free(tmp);
        return (CONTINUE_READ);
    }
}

やってることとしては

  • 前回読み込んだ文字列の残り save に改行が入っている場合は line に改行文字までの文字列をコピーし, 残りの文字列を save に代入してSUCCESSを返す.
  • 前回読み込んだ文字列の残り save に改行が入っていない場合は linesave の文字列を全てコピーする.

static int read_process(int fd, char **line, char **save);

read() 関数を呼び出し, 読み込まれたデータを save に保存する. 読み込んだ文字列に改行が入っていた場合にはそこまでを line に入れて, 残りを save に入れる.

static int read_process(int fd, char **line, char **save)
{
    ssize_t        read_size;
    int            ret;
    char       *buf;

    ret = CONTINUE_READ;
    if (!(buf = malloc(BUFFER_SIZE + 1)))
        return (ERROR);
    while (ret == CONTINUE_READ && (read_size = read(fd, buf, BUFFER_SIZE)) > 0)
    {
        buf[read_size] = '\0';
        if (ft_strchr(buf, '\n'))
            ret = split_by_newline(line, save, buf);
        else
            ret = join_line_and_buf(line, buf);
    }
    free(buf);
    if (ret == CONTINUE_READ && read_size == 0)
        ret = END_OF_FILE;
    else if (ret == CONTINUE_READ && read_size == -1)
        ret = ERROR;
    return (ret);
}

やっていることとしては

  • ret == CONTINUE_READ だったら fdからBUFFER_SIZE分文字を読み込み buf に入れる.
  • もし buf の中に改行文字が入っていたら split_by_newline() を呼び出して改行文字までを line と結合し, 残りを save に入れてループを抜ける. (ret == SUCCESS になるから)
  • もし buf の中に改行文字が入っていなかったら join_line_and_buf() を呼び出して単純に linebuf を結合する.
  • ループ処理終了後, もし ret == CONTINUE_READ && read_size == 0 だったら, これすなわち改行は見つからずにfdがEOFに達したということを意味するので END_OF_FILE を返す.
  • read_size == -1 ということは読み込みに失敗しているのでERRORを返す.
  • 上記2つのif文の条件を通過したということは正常に終了したかEOFに到達したということなのでそのまま ret を返す.

static int split_by_newline(char **line, char **save, char *buf);

buf 内で最初に出現した改行文字より前を line に, それより後ろを save に格納する.

static int split_by_newline(char **line, char **save, char *buf)
{
    char   *old_line;
    char   *tmp;
    char   *newline_ptr;

    newline_ptr = ft_strchr(buf, '\n');
    if (!(tmp = ft_substr(buf, 0, newline_ptr - buf)))
        return (ERROR);
    old_line = *line;
    *line = ft_strjoin(*line, tmp);
    free(old_line);
    free(tmp);
    if (!(*line))
        return (ERROR);
    if (!(*save = ft_substr(newline_ptr + 1, 0,
                                ft_strlen(newline_ptr + 1))))
        return (ERROR);
    return (SUCCESS);
}

やっていることとしてはこの関数の説明をそのままコードにしただけなので詳細な説明は省く.

static int join_line_and_buf(char **line, char *buf);

linebuf を単純に結合するだけ.

static int join_line_and_buf(char **line, char *buf)
{
    char   *tmp;

    tmp = *line;
    *line = ft_strjoin(*line, buf);
    free(tmp);
    if (!(*line))
        return (ERROR);
    return (CONTINUE_READ);
}

やっていることとしてはこの関数の説明をそのままコードにしただけなので詳細な説明は省く.

get_next_line_utils.c

以下の関数が定義されている.

char    *ft_strdup(const char *s);
char   *ft_substr(char const *s, unsigned int start, size_t len);
size_t ft_strlen(const char *s);
char   *ft_strchr(const char *s, int c);
char   *ft_strjoin(char const *s1, char const *s2);

基本的には ft_ というプレフィックスを取った標準ライブラリ関数と同じ動きをする. (例: ft_strdup() -> strdup())

これらに関しては標準ライブラリ関数とほぼ同じなので実装レベルで説明は行わない.

詳細を知りたい場合は以下のリンクから見ることが出来る.

github.com

動作例

実際の動きを見ていく.

以下のような main.c を用意する.

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "get_next_line.h"

int main(int argc, char *argv[])
{
    char *file_name;
    if (argc == 1)
    {
        file_name = "test.txt";
    }
    else
        file_name = argv[1];
    int fd = open(file_name, O_RDONLY);
    // int fd = 999999;
    char *line;
    int status;

    printf("\n[START] %s\n\tfd: %d\n\n", file_name, fd);
    do{
        status = get_next_line(fd, &line);
        printf("status:  \t%d\n", status);
        printf("read txt:\t%s\n", line);
        free(line);
    }while (status == 1);
    printf("\n[FINISH] %s\n\tstatus: %d\n\n", file_name, status);
    
    close(fd);
}

そしてこれをコンパイルする gcc -D BUFFER_SIZE=256 main.c get_next_line.c get_next_line_utils.c

./a.out で実行するとtest.txtから1行ずつ読み込めていることがわかるだろう.

ちなみに./a.out の引数としてファイル名を指定することもでき, ./a.out main.c とすると main.c 自身が1行ずつ読み込まれて出力される.

[START] main.c
        fd: 3

status:         1
read txt:       #include <stdio.h>
status:         1
read txt:       #include <sys/types.h>
status:         1
read txt:       #include <sys/stat.h>
status:         1
read txt:       #include <fcntl.h>
status:         1
read txt:       #include "get_next_line.h"
status:         1
read txt:
status:         1
read txt:       int main(int argc, char *argv[])
status:         1
read txt:       {
status:         1
read txt:               char *file_name;
status:         1
<---------------------  長いので中略  ------------------------------->
read txt:               }while (status == 1);
status:         1
read txt:               printf("\n[FINISH] %s\n\tstatus: %d\n\n", file_name, status);
status:         1
read txt:
status:         1
read txt:               close(fd);
status:         1
read txt:       }
status:         0
read txt:

[FINISH] main.c
        status: 0

BUFFER_SIZE による実行時間とシステムコール呼び出し回数の変化

また, コンパイル時に指定していた定数BUFFER_SIZEを変更することで一度のread()で読み込む量が変わる.

実際にBUFFER_SIZEの値を変更するとどのくらい変わるのか, システムコールが呼び出された回数が見れるコマンド strace を使って計測してみた.

$ gcc -D BUFFER_SIZE=1 main.c get_next_line.c get_next_line_utils.c && strace -c -e trace=read  ./a.out main.c
[START] main.c
<---------------------  長いので中略  ------------------------------->
[FINISH] main.c
        status: 0

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.001448           2       634           read
------ ----------- ----------- --------- --------- ----------------
100.00    0.001448                   634           total

BUFFER_SIZE=1の時, システムコールread() が 634回呼ばれています.

BUFFER_SIZE=10に設定して同様のコマンドでコンパイルして実行すると

$ gcc -D BUFFER_SIZE=10 main.c get_next_line.c get_next_line_utils.c && strace -c -e trace=read  ./a.out main.c
[START] main.c
<---------------------  長いので中略  ------------------------------->
[FINISH] main.c
        status: 0

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.00    0.000000           0        66           read
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                    66           total

BUFFER_SIZE=100にすると以下のようになります.

$ gcc -D BUFFER_SIZE=100 main.c get_next_line.c get_next_line_utils.c && strace -c -e trace=read  ./a.out main.c
[START] main.c
<---------------------  長いので中略  ------------------------------->
[FINISH] main.c
        status: 0

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.000038           4         9           read
------ ----------- ----------- --------- --------- ----------------
100.00    0.000038                     9           total

BUFFER_SIZE を大きくするとシステムコール read() が呼ばれる回数が少なくなっているのがわかりますね!

試しにめっちゃでかいファイルを作って, それの読み込み速度を比較してみましょう.

$ for i in `seq 1 1000000`  # めっちゃでかいファイルを作る
do
echo "${i}" >> big_file.txt
done
$
$ ls -lh big_file.txt  # ファイルサイズは6.6MB
-rw-rw-r-- 1 jun jun 6.6M 12月 18 06:45 big_file.txt
$
$ wc -m big_file.txt  # 文字数は6888896文字
6888896 big_file.txt

BUFFER_SIZE を変えてテストしてみましょう.

なお, このテストでは main.c 内にあるwhileループ内のprintfコメントアウトして実行するようにしています.

テストは以下のような形で行い, total の時間を見ます. ついでに read() が呼ばれた回数も見ます.

$ gcc -D BUFFER_SIZE=100 main.c get_next_line.c get_next_line_utils.c && time ./a.out big_file.txt && strace -c -e trace=read  ./a.out big_file.txt

[START] big_file.txt
        fd: 3


[FINISH] big_file.txt
        status: 0

./a.out big_file.txt  1.03s user 0.02s system 99% cpu 1.050 total

[START] big_file.txt
        fd: 3


[FINISH] big_file.txt
        status: 0

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.058994           0     68891           read
------ ----------- ----------- --------- --------- ----------------
100.00    0.058994                 68891           total

BUFFER_SIZE=1 の時は 5.56秒でした.

このような感じで何回かBUFFER_SIZEを変更して実行時間の変化を見ます.

BUFFER_SIZE 実行時間[s] read()が呼ばれた回数
1 5.6 6888898
10 0.742 688892
100 1.051 68891
1000 9.4 6891
10000 101 691

うーむ, BUFFER_SIZE=10 が一番速いですね...

イメージとしてはBUFFER_SIZEが大きくなればなるほど早くなると思ったのですが...

プロファイルツールを用いたボトルネックの特定

gcc 付属のプロファイルツール gprof を使ってどの関数がどれくらい呼ばれて, どのくらい時間がかかったかを計測してみました.

$ gcc -pg -D BUFFER_SIZE=1000 main.c get_next_line.c get_next_line_utils.c && ./a.out big_file.txt && gprof ./a.out 

  %   cumulative   self              self     total
 time   seconds   seconds    calls  us/call  us/call  name
 78.50      7.14     7.14  6012695     1.19     1.19  ft_strlen
 21.31      9.08     1.94  2000000     0.97     3.34  ft_substr
  0.33      9.11     0.03  1013778     0.03     0.03  ft_strchr
  0.22      9.13     0.02  1000001     0.02     9.14  get_next_line
  0.11      9.14     0.01  1000000     0.01     7.86  join_line_and_save
  0.00      9.14     0.00  1000001     0.00     1.19  ft_strdup
  0.00      9.14     0.00     6890     0.00    10.31  read_process
  0.00      9.14     0.00     6889     0.00     2.38  ft_strjoin
  0.00      9.14     0.00     6889     0.00    10.28  split_by_newline

おおぉ... 自分としてはてっきりメモリコピー系が時間かかっているかと思ったのですが, まさかのft_strlenが一番のボトルネックだったとは...

ft_strlen() の実行回数が多いのは, get_next_line_utils.c 内で大量に ft_strlen() 呼んでいるからでしょうな...

ちょっとgrepして, ft_strlen() を使用している場所を抜き出してみた

get_next_line.c:28:             *save = ft_substr(newline_ptr + 1, 0, ft_strlen(newline_ptr + 1));
get_next_line.c:60:                                                             ft_strlen(newline_ptr + 1))))
get_next_line_utils.c:31:       s_len = ft_strlen(s);
get_next_line_utils.c:53:       if (start >= ft_strlen(s))
get_next_line_utils.c:56:       s_len = ft_strlen(s);
get_next_line_utils.c:92:       total_len = ft_strlen(s1) + ft_strlen(s2);

なるほど, 意外と呼び出していないように見えますね... しかし, 多分呼び出し元の関数がかなりの回数呼ばれていて, それが累積してこの結果になったんだと思います.

ここでスパッと解決出来ればかっこいいのですが, ちょっと解決策が思い浮かばないので高速化の方法がある人はぜひ教えてください.

(2020/12/22 追記) ft_strlenを標準ライブラリ実装に置き換えてみる

先程ft_strlen()の実行時間がプログラム全体の実行時間の中で大きな割合を示すことを調査しました.

じゃあ標準Cライブラリのstrlen()使ったコードに置き換えたらどうなるか調べてみました.

BUFFER_SIZE 実行時間[s](ft_strlen) 実行時間[s](strlen)
1000 9.4 2.6
10000 101 18.9

めっちゃ速くなった!!

BUFFER_SIZE=1000 の時の実行時間内訳ではft_strlen()が7秒かかっていたのですが, 標準実装に変えたらプログラム全体の実行時間が7秒減ったことを考えるとft_strlen()でかかっていた7秒がまるまる速くなった感じですね.

ちなみにgprofを使って実行時間の内訳を見ると...

$ gcc -pg -D BUFFER_SIZE=1000 main.c get_next_line.c get_next_line_utils.c && ./a.out big_file.txt && gprof ./a.out

  %   cumulative   self              self     total
 time   seconds   seconds    calls  us/call  us/call  name
 98.70      1.68     1.68  2000000     0.84     0.84  ft_substr
  1.18      1.70     0.02  1000000     0.02     1.69  join_line_and_save
  0.59      1.71     0.01                             main
  0.00      1.71     0.00  1013778     0.00     0.00  ft_strchr
  0.00      1.71     0.00  1000001     0.00     0.00  ft_strdup
  0.00      1.71     0.00  1000001     0.00     1.70  get_next_line
  0.00      1.71     0.00     6890     0.00     1.68  read_process
  0.00      1.71     0.00     6889     0.00     0.00  ft_strjoin
  0.00      1.71     0.00     6889     0.00     1.68  split_by_newline

ft_strlen()が7秒かかっていたのが消えて, 一番時間がかかった関数がft_substr()になってますね.

ちなみに自分の ft_strlen()は以下のようになっており, 特別遅いことをやっているわけでは無いと思うので, 標準Cライブラリの実装が恐ろしく速いのだと思います.

size_t  ft_strlen(const char *s)
{
    size_t ans;

    ans = 0;
    while (s[ans])
        ans++;
    return (ans);
}

ちなみに標準Cライブラリ(glibc)の実装は以下のリンクに載っています. 自分は読んでも何をしているかわからなかったので, わかったらまた記事にしたいと思います.

github.com

ボーナス課題 (複数fd対応)

42の課題にはボーナスというものがあります. これは今までの必須パートをクリアした人で余裕があれば挑戦してね〜って感じの内容です.

今回の課題のボーナスは複数fd対応です.

どういうことかというと, fd=3, fd=4, fd=5 を順番にget_next_line()に渡して呼び出した際に, 今の実装だったら save が1つしかないので順番に呼び出すと他のfdsaveを上書きしてしまって上手くいかないので, なんとかして動くようにしましょう! という課題です.

実装

今回は以下の線形リストを表す構造体を定義し, これをうまく使って実装しました.

typedef struct s_list
{
    int                fd;
    char           *save;
    struct s_list  *next;
}               t_list;

基本的に実装は必須パートと同じなのですが, get_next_line() を結構変更したのと, 線形リスト用の関数(create_fd_elem())を作成したのでその2つだけ説明します.

t_list *create_fd_elem(t_list **lst, int fd);

t_list   *create_fd_elem(t_list **lst, int fd)
{
    t_list *new;

    if (!lst)
        return (NULL);
    if (!(new = malloc(sizeof(t_list))))
        return (NULL);
    new->fd = fd;
    new->save = NULL;
    if (!(*lst))
    {
        *lst = new;
        new->next = NULL;
    }
    else
    {
        new->next = *lst;
        *lst = new;
    }
    return (new);
}

create_fd_elem() は新しいfdが来た時に構造体を動的に確保し, それを初期化してlstの先頭要素にして, 作成した要素を返す関数です.

int get_next_line(int fd, char **line)

int         get_next_line(int fd, char **line)
{
    int                ret;
    static t_list  *save_list_head;
    t_list          *target_save_list;

    if (fd < 0 || !line || BUFFER_SIZE <= 0 || !(*line = ft_substr("", 0, 0)))
        return (ERROR);
    target_save_list = save_list_head;
        // fd のリスト要素を探す
    while (target_save_list && target_save_list->fd != fd)
        target_save_list = target_save_list->next;
        // 見つからなかった場合はリスト要素を作成
    if (!target_save_list)
        if (!(target_save_list = create_fd_elem(&save_list_head, fd)))
            return (ERROR);
    ret = CONTINUE_READ;
    if (target_save_list->save)
        ret = join_line_from_save(line, &target_save_list->save);
    if (ret == CONTINUE_READ)
        ret = read_process(fd, line, &target_save_list->save);
    if (ret == END_OF_FILE || ret == ERROR)
    {
        free(target_save_list->save);
                // 今回のfdが先頭要素だった場合, 先頭要素を次の要素に変更
        if (save_list_head == target_save_list)
            save_list_head = target_save_list->next;
        else
        {
                        // 要素を削除した上でその要素の前後の要素を接続する
            t_list *tmp = save_list_head;
            while (tmp->next != target_save_list)
                tmp = tmp->next;
            tmp->next = target_save_list->next;
        }
        free(target_save_list);
    }
    return (ret);
}

注意: if (ret == END_OF_FILE || ret == ERROR) 直下の else文 は提出時ありませんでした. なので提出後に修正したものなのでこのままだとNorminetteの行数制限に引っかかります.

今回は行数が多く, 説明が少しむずかしかったのでコメントという形で説明しました.

動作例

複数ファイルを並行して読み込むようなmain.cを作成します.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include "get_next_line_bonus.h"

int main()
{
    char *line;
    int fd1 = open("./get_next_line.h", O_RDONLY);
    int fd2 = open("./get_next_line.c", O_RDONLY);
    int fd3 = open("./get_next_line_utils.c", O_RDONLY);
    int status;
    printf("fd1: %d\n", fd1);
    printf("fd2: %d\n", fd2);
    printf("fd3: %d\n", fd3);
    int finish_count = 0;
    int idx = 0;
    while (finish_count < 3)
    {
        if (idx % 3 == 0)
        {
            printf("\n---------- fd1: %d ----------\n", fd1);
            
            status = get_next_line(fd1, &line);
            printf("status:  \t%d\n", status);
            printf("read txt:\t%s\n", line);
            free(line);
            if (status == 1)
                finish_count = 0;
            else
                finish_count++;
        }
        else if(idx % 3 == 1)
        {
            printf("\n---------- fd2: %d ----------\n", fd2);
            status = get_next_line(fd2, &line);
            printf("status:  \t%d\n", status);
            printf("read txt:\t%s\n", line);
            free(line);
            if (status == 1)
                finish_count = 0;
            else
                finish_count++;
        }
        else if (idx % 3 == 2)
        {
            printf("\n---------- fd3: %d ----------\n", fd3);
            status = get_next_line(fd3, &line)  ;
            printf("status:  \t%d\n", status);
            printf("read txt:\t%s\n", line);
            free(line);
            if (status == 1)
                finish_count = 0;
            else
                finish_count++;
        }
        idx++;
    }
    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}

そしてこの main.cコンパイルして実行すると複数ファイル並列で行ごとに読み込めていることがわかると思います.

$ gcc -D BUFFER_SIZE=1000 main_multiple_fd.c get_next_line_bonus.c  get_next_line_utils_bonus.c && ./a.out
fd1: 3
fd2: 4
fd3: 5

---------- fd1: 3 ----------
status:         1
read txt:       #ifndef GET_NEXT_LINE_H

---------- fd2: 4 ----------
status:         1
read txt:       #include "get_next_line.h"

---------- fd3: 5 ----------
status:         1
read txt:       #include "get_next_line.h"

---------- fd1: 3 ----------
status:         1
read txt:       # define GET_NEXT_LINE_H

---------- fd2: 4 ----------
status:         1
read txt:

---------- fd3: 5 ----------
status:         1
read txt:

---------- fd1: 3 ----------
status:         1
read txt:

---------- fd2: 4 ----------
status:         1
read txt:       static int      join_line_and_save(char **line, char **save)

---------- fd3: 5 ----------
status:         1
read txt:       size_t  ft_strlen(const char *s)

---------- fd1: 3 ----------
status:         1
read txt:       # include <stdlib.h>
<-----------長いので中略----------->
---------- fd1: 3 ----------
status:         0
read txt:

---------- fd2: 4 ----------
status:         1
read txt:               return (ret);

---------- fd3: 5 ----------
status:         0
read txt:

---------- fd1: 3 ----------
status:         0
read txt:

---------- fd2: 4 ----------
status:         1
read txt:       }

---------- fd3: 5 ----------
status:         0
read txt:

---------- fd1: 3 ----------
status:         0
read txt:

---------- fd2: 4 ----------
status:         0
read txt:
status: 0

static変数について

今回の課題の意図の1つとして static変数 を理解するというのがあるので, 自分なりに調べたり試したりした結果も書いておく. なお, ここでいう static変数 とは関数内のものを指す点に留意して頂きたい.

  • static変数はグローバル変数と同じメモリの静的領域に配置される.
  • static変数は宣言されたと同時に初期化する必要がある. 例: static a = 10
    • 明示的に初期化しない場合は0で初期化される.

以下のサイトがメモリ領域についてわかりやすかったです.

www.momoyama-usagi.com

あとWikipediaの以下の説明もわかりやすいです.

ローカル変数宣言にstaticキーワード(静的記憶クラス指定子)を付加すると、「静的ローカル変数」[3]となり、変数寿命はプログラムの生存期間と同一となる。C言語では静的ローカル変数はグローバル変数と同じくプログラム開始処理以前に一度だけ初期化される

ja.wikipedia.org

完走した感想

完走した感想(激うまギャグ) ですが, 我ながらうまく書けたと思います.

特にボーナスパートに関しては, 42Tokyo内でもリストを使った実装をした人は少なかったので, ドヤッ! って感じです. ちなみに点数は線形リストの後始末にミスがあり 113/115 点でした (必須パート100点 + ボーナス15点 計算)

自分は基本的に何かしら実装をしたり, 何か開発したりした際にはブログにこうやってアウトプットしたいマンなのですが, 当初42Tokyo内では課題についてどこまで公開してよいか明確な基準は無く, 記事を書くかためらっていました. しかし, 1ヶ月ほど前に公式からルールを守ればOKというのを得たので遠慮なくコードの解説を書きまくりました.

しかし, 書いた本人が言うのもなんですが, 42の課題は自分で考えて調べてやり方を試して実装して, そしてクリア! というのが一番楽しい42の課題の遊び方だと思います. なので申し訳程度にまえがきで注意喚起をしました.

本記事に関してですが, ここまで書くのに5時間ほどかかりました. もともとこんなにかかる予定では無かったのですが, BUFFER_SIZE による時間とシステムコールの呼び出し回数, プロファイラによるパフォーマンス解析などに手を出したらこんなに時間がかかりました.

あとは単純に最近ブログでこういう技術系の記事を書くことが少なかったので, 綺麗な文章を一発で書けなくなってるんですよね. まぁ, アウトプット量を増やせば自然にある程度早く記事が書けるようになるので, これからもいっぱいアウトプットしていきます. ただ, このレベルの文量を毎回書くのはしんどい(特にコードの解説とかインデントの修正とか)ので次からは多分文量が減ると思います.

42ではこのような課題が沢山あり, 人と一緒に相談したり楽しみながらコンピューターサイエンスを手を動かして学べる環境が整っているので自分としては最高に楽しいです. (この課題がまだLv1というのが最高にワクワクしますね)

42は名前だけは調べれば出てくるけど, 中で何をやっているかは正直謎に包まれている部分が多いと思います(課題の情報をアウトプットする人も多くはないですし). なので, 自分の記事を呼んで「へ〜42ってこういう感じなんだな」というのを感じて貰えれば良いなと思います.

42に興味があればとりあえず42の入学試験である Piscine だけでも受けて見ると良いと思います.

参考にしたサイト