Linuxカーネルを読む前にやったこと
「カーネルのコードがよくわからない。Linuxカーネルに関する本を読んでもいまいちしっくりこない。」 から、「読めば理解できそう..!」 になるまでにやったことのまとめ。
はじめに
低レイヤの話がわかるようになりたかった。 カーネルの中身が知りたかった。 とりあえず本を読もうと思い詳解 Linuxカーネル 第3版を読んだが知識がなさ過ぎてよくわからない。 知らない用語だらけで都度調べればなんとなくはわかる気もするが、いまいち頭に入ってこない。 今思うとそもそもCPUの話なのかカーネルの話なのかさえよくわからない状態で読んでいたような気がする。
そんな状態を克服するためにやったことをまとめておく。
学習前
学習前の自分の知識はこんな感じだった。
知っていた
よく知らなかった
補足:
- 大学は情報学部ではなく工学部卒。OSのことはほとんど学んでいなかった。
- 仕事はWebアプリケーション開発がメイン
何をやったか
ここからがメイン。何をしたか、どのような効果があったか。
1. CPUの基礎の学習
学習内容
- CPUの創りかたを読んだ。高校理科程度の知識から簡単なCPUを作れるようになる本。
効果
- 本当に簡単なCPUなら作れるのでは、という気持になった。
- CPUのイメージができるようになりレジスタやアセンブリ周りの話が理解しやすくなった気がする。
- FPGAにも手を出したくなった…が、時間の制約で保留とした
2. コンパイラの作成
学習内容
- ふつうのコンパイラをつくろうと8ccを参考に、Cの文法を勉強しつつ簡単なCコンパイラを書いた。
- ふつうのコンパイラをつくろうはC言語とほぼ同様の言語のコンパイラをJavaで作る本。8ccについてはこちらの記事を。
- そのまま本をトレースしても面白くないのでパーサジェネレータ(yacc,javaCCなど)も使わずPythonで実装した。
- 8ccのリポジトリのログをものすごく参考にした。(ruiuさん、ありがとうございました)
効果
- アセンブリに慣れた。
- スタックの使い方に慣れた。
- Calling Conventionを知った。(後のOS開発の役に立った)
3. OSの学習(1)
学習内容
- 30日でできる! OS自作入門を参考にOSを作成した。Windowsで開発する前提で書かれていたが、全てUbuntuで動くよう修正しながら進めた。そのせいで詰まったがそれがよい学びになった。
- CPUについての理解を深めるため32ビットコンピュータをやさしく語る はじめて読む486 (アスキー書籍)も一緒に読んだ。
- 進めたときのログはこちら
効果
- GDT、システムコール、割り込み…などの用語がOSから見た意味でわかるようになった。
- CPUの機能を知った。
4. OSの学習(2)
学習内容
- MITのOSの授業を進めた。 内容はメモリ管理、ページテーブル、システムコール、ネットワークドライバなどのコードを自分で書いていってOSを作っていくというもの。ほぼ機能のないOSが最終的にはhttpサーバが動くようになる。課題の答えはないがテストコードがあり、それで自分のコードを確認できる。 scheduleに沿ってすすめると、次の課題までにこれを読め、など適切なテキストも与えてくれる。もちろん全部英語。
- 進めたときのログはこちら
効果
まとめ
CPUの本を読んでコンパイラ作ってOSを作ったら、低レイヤの話が以前よりもわかるようになり、詳解 Linuxカーネル 第3版を見ながらLinuxのコードも追えるようになった。
上の内容を全部やると結構時間がかかるので、強くおすすめするわけではありませんが、この辺りを学ぼうとしている人の参考になれば幸いです。
- 作者: 渡波郁
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2003/10/01
- メディア: 単行本(ソフトカバー)
- 購入: 35人 クリック: 445回
- この商品を含むブログ (192件) を見る
- 作者: 青木峰郎
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2009/07/24
- メディア: 単行本
- 購入: 25人 クリック: 398回
- この商品を含むブログ (48件) を見る
- 作者: 川合秀実
- 出版社/メーカー: 毎日コミュニケーションズ
- 発売日: 2006/03/01
- メディア: 単行本
- 購入: 36人 クリック: 735回
- この商品を含むブログ (299件) を見る
32ビットコンピュータをやさしく語る はじめて読む486 (アスキー書籍)
- 作者: 蒲地輝尚
- 出版社/メーカー: KADOKAWA / アスキー・メディアワークス
- 発売日: 2014/10/21
- メディア: Kindle版
- この商品を含むブログを見る
- 作者: Daniel P. Bovet,Marco Cesati,高橋浩和,杉田由美子,清水正明,高杉昌督,平松雅巳,安井隆宏
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2007/02/26
- メディア: 大型本
- 購入: 9人 クリック: 269回
- この商品を含むブログ (71件) を見る
Rubyのメソッド周りの動作まとめ
今までRubyにあまり触れていなかったため、 初めてのRuby、 メタプログラミングRuby 第2版、 Rubyのしくみ -Ruby Under a Microscope- を一気に読んだ。
この記事の内容は、学習の記録&復習としてメソッド周りの動作をまとめたもの。
公式ドキュメントは読んだがそれ以上はやってないという方にはもしかしたら役に立つかもしれない。 もうすでにバリバリ使ってる方には当たり前の内容かもしれない。
コードの動作は
$ ruby -v ruby 2.3.1p112 (2016-04-26) [x86_64-linux-gnu]
で確認した。
目次
- メソッドの実行
- メソッドの探索
- メソッドの定義
メソッドの実行
メソッドの呼び出し時の動作:
- 常にレシーバが設定される
- レシーバを明示しない場合は呼び出したスコープでの
self
がレシーバとなる - メソッド内の
self
は呼び出されたときのレシーバとなる
def whoami(); self; end whoami # => main //トップレベルのselfはmainオブジェクト(Objectクラスのインスタンス) class C whoami # => C def f whoami end end o = C.new o # => #<C:0x000000008c7c88> o.f # => #<C:0x000000008c7c88> // レシーバはo. whoami内のselfはo
send
を使えばメソッド名のシンボルを使って呼び出すこともできる。
その際のレシーバはsend
のレシーバが使われる。
# つづき o.send(:f) # => #<C:0x000000008c7c88> // レシーバはo. whoami内のselfはo
メソッドの探索
オブジェクトは内部で
- そのクラスへの参照をもつ
- スーパークラスへの参照をもつ(クラスオブジェクトの場合)
これらの参照先はclass
メソッドやsuperclass
メソッドで得られるものとは異なる。
説明のため以下ではそれらの参照を単にそれぞれklass, superという言葉で表す。
(klass、superはRubyのC言語での実装で使われているポインタの名前。
もしかしたら新しいバージョンでは実装や名前が変わっているかもしれないが、大きく実装が変わらない限り当分このイメージで問題ないだろう)
obj.method_name()としてメソッドを実行すると、
- objのklassが指すクラスにmethod_nameが定義されているか
- そのsuperのクラスに定義されているか
- そのsuperのクラスに定義されているか
- …
と探索し、最初に見つかったものを実行する。
ancestors
でそのsuperのチェーンを確認できる。
class P; end module M; end module N include M # N -super-> M end N.ancestors # => [N, M] class C < P # C -super-> P include N # C -super-> N -super-> M -super-> P end C.ancestors # => [C, N, M, P, Object, Kernel, BasicObject] N.ancestors # => [N, M] // インクルード時にはmoduleのコピーをインクルードするのでここで M -super-> Pになっているわけではない
メソッドを探索し最後まで見つからなければ、最初に戻ってmethod_missing
メソッドを探索する。
BasicObject
クラスにはもともとmethod_missing
が定義されているため何もしなければそれが実行される。
BasicObject.private_instance_methods(false) # => [..., :method_missing, ...]
method_missing
を定義すれば存在しないメソッドを処理することもできる。(ゴーストメソッドとよばれる)
class C def method_missing(name) "#{name} is called!" end end C.new.hogehoge # => "hogehoge is called!" C.new.helloworld # => "helloworld is called!"
メソッドの定義
def
によってメソッドを定義する方法は2つ
def
+ メソッド名で定義(def methodname() ...
)def
+ オブジェクト名.メソッド名 で定義(def obj.methodname() ...
)
1. def methodname() ...
で定義
この場合、def
が現れた場所のカレントクラスのインスタンスメソッドとして定義される。
("カレントクラス"はメタプログラミングRuby 第2版で使われていた用語で、公式の用語ではなさそう)
カレントクラスはスコープが参照しているクラスのことで、次のようになっている。
# カレントクラス: Object module M # カレントクラス: M class C # カレントクラス: C. hogeはCのインスタンスメソッドとなる def hoge() # カレントクラス: self.class end end # カレントクラス: M end p M::C.instance_methods(false) # => [:hoge]
2. def obj.methodname() ...
で定義
この場合、そのobjの固有のクラス(特異クラス)のインスタンスメソッドに定義される。 (以下、objオブジェクトの特異クラスには#を付けて#objと表す)
class C def func end end obj = C.new obj2 = C.new def obj.sfunc # objの特異クラス#objのインスタンスメソッドとして定義 end obj.sfunc obj2.sfunc # Error p obj.class.instance_methods(false) #=> [:func] p obj.singleton_class.instance_methods(false) #=> [:sfunc]
上記のobj.sfunc
はobj
の特異メソッド(singleton method)と呼ばれる。
このときメソッドの探索で説明したklassとsuperは
obj -klass-> #obj -super-> C -super-> ...
のようになっており、#objにsfunc、Cにfuncが定義されているため、前述のメソッドの探索でobj.func
もobj.sfunc
も実行できる。
obj2.sfunc
がErrorなのはその探索するクラスにsfunc
が定義されていないため。
クラスでは…
クラスもオブジェクトであるため、
def C.classfunc end
とすればクラスの特異メソッド(クラスメソッド)が定義でき、C.classfunc
で実行できる。
class ClassName ... end
内のselfはそのクラスになることから、上記は以下のようにも定義できる。
class C def self.classfunc # ここでのselfはC. Cの特異クラスに定義 end end
class << object
とするとそのオブジェクトの特異クラスをカレントクラスとしてオープンできるので、下のようにもかける。
class << C # このスコープでのカレントクラスはCの特異クラス def classfunc end end
クラスの特異クラスのことをメタクラスとよぶこともある。
スーパークラスの特異クラスは特異クラスのスーパークラスなので、クラスメソッドはサブクラスでも実行できる。
class P def self.classf "ok" end classf # => "ok" end class C < P classf # => "ok" // selfはC. C -klass-> #C -super-> #P end C.singleton_class.superclass == P.singleton_class # => true
P --klass-> #P (def classf) ^ ^ super super ^ ^ C --klass-> #C
クラスがオブジェクトであるように、特異クラスもオブジェクトであるため特異クラスの特異クラスも作成できる。(使い道があるかどうかは別として)
トップレベル
トップレベルでのカレントクラスはObjectであり、 トップレベルでオブジェクト指定なしでメソッドを定義するとObjectのprivateメソッドとして定義される。 この事実と、
- レシーバを指定せずにメソッドを実行すると
self
がレシーバとして設定される Object
はあらゆるクラスのスーパークラス
ということから、トップレベルで定義した関数はあらゆる場所でレシーバなし実行できる。
(Object
のサブクラスではないクラスも作れるため、全ての場所で実行できるわけではない。)
def top; "ok!" end Object.private_instance_methods(false) # => [..., :top, ...] class C top() # => ok! // selfはC。 CはClassクラスのインスタンス def hoge top() end end C.new.hoge # => ok! // hoge内のselfはCクラスのインスタンス class Clean < BasicObject top() # => ok! // selfはClean, CleanはClassクラスのインスタンス def fuga top() end end Clean.ancestors # => [Clean, BasicObject] Clean.new.fuga # NoMethodError // topがundefined. selfのメソッド探索ルートにObjectがいないため
ちなみにprivateメソッドはレシーバを明示できないため(privateの制約)、 トップレベルのメソッドはレシーバなし実行できるというよりは、 レシーバを明示したobj.method()の形式では実行できない。
特異クラスはいつ作られるのか(おまけ)
p ObjectSpace.count_objects[:T_CLASS] # => 612 class C; end p ObjectSpace.count_objects[:T_CLASS] # => 614 // Cと#Cが作られた? o = C.new p ObjectSpace.count_objects[:T_CLASS] # => 614 def o.f; end p ObjectSpace.count_objects[:T_CLASS] # => 615 // #oがつくられた?
クラスの数だけで判断すると、クラスの特異クラスはクラス定義時に作成される、 クラスでないオブジェクトの特異クラスは必要になったら作成される、っぽい。 (前半の、クラス定義時に2つクラスができる、というのはRubyのしくみ -Ruby Under a Microscope-に記載されている内容。)
- 作者: Yugui
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/06/26
- メディア: 大型本
- 購入: 27人 クリック: 644回
- この商品を含むブログ (251件) を見る
- 作者: Paolo Perrotta,角征典
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/10/10
- メディア: 大型本
- この商品を含むブログ (3件) を見る
Rubyのしくみ -Ruby Under a Microscope-
- 作者: Pat Shaughnessy,島田浩二,角谷信太郎
- 出版社/メーカー: オーム社
- 発売日: 2014/11/29
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (4件) を見る
Linuxカーネル コードリーディング ( kernel/sched.c )
詳解 Linuxカーネル 第3版とLinuxカーネル2.6解読室を参考に Linuxカーネルのkernel/sched.cのschedule()の一部を読んだのでその記録を残しておきます。
この記事はただのコードリーディングのログで、内部を知りたい人向けではありません。 これから読もうと思っている人への何かしらのヒントになったらいいかなと思って公開しています。 (※実際ここに記載するスケジューラのアルゴリズムは2.6.23以降使われていません。 参考:Completely Fair Scheduler の内側)
バージョンはv2.6.12、ハードウェア依存のコードはi386のものです。
なぜ読んだか
- kernelのコードを読むということに慣れておきたい
- ついでに何か発見があったらラッキー
ただなんとなく読んでみたかった、という気持ちも大きいです。
なぜこの部分を選んだか
- 上記2つの書籍で解説があったので困ったら頼れる
- スケジューリングのアルゴリズムなら「○○とは」のような事前知識がなくても読めそう
ここからコードリーディング
kernel/sched.c
内のschedule
関数を読みます。
わかりそうな所はちゃんと読みつつ、難しそう、または今読まなくてもよさそうな所は後回しにして気楽に読んでいきます。
関数全体はこんな感じです。
https://github.com/komukomo/linux/blob/v2.6.12-read/kernel/sched.c#L2607
…長いので一旦全部忘れて、重要そうなところを抜き出します。
asmlinkage void __sched schedule(void) { task_t *prev, *next; runqueue_t *rq; ... prev = context_switch(rq, prev, next); ... }
変数宣言と1つの関数呼び出しのみになりました。 コンテキストスイッチ、つまりあるプロセスから次のプロセスへの切り替えをしています。これがこの関数で最終的にやりたいことです。 (プロセスやらスレッドやら紛らわしいので以降ここではプロセスではなくtaskと呼ぶことにします)
とりあえずcontext_switch
の中身は触れず、次にrq
,next
,prev
に代入されているコードを埋めてみます。代入箇所は複数ありますが、最も意味のありそうな行を書いておきます。
asmlinkage void __sched schedule(void) { task_t *prev, *next; runqueue_t *rq; struct list_head *queue; int idx; ... prev = current; ... rq = this_rq(); ... next = list_entry(queue->next, task_t, run_list); ... prev = context_switch(rq, prev, next); ... }
prev
はcurrent
を代入しているだけなので簡単そうです。名前からして現在のtaskを表しているようですが、一応中身を見てみます。
include/asm-i386/current.h
static inline struct task_struct * get_current(void) { return current_thread_info()->task; } #define current get_current()
include/asm-i386/thread_info.h
// how to get the thread information struct from C static inline struct thread_info *current_thread_info(void) { struct thread_info *ti; __asm__("andl %%esp,%0; ":"=r" (ti) : "0" (~(THREAD_SIZE - 1))); return ti; }
インラインアセンブラ構文が現れました。
インラインアセンブラとは、cのコードの中にアセンブリを直接書くことのできる機能です。
ここではespレジスタ(現在のスタックのトップを指しているもの)を使ってthread_info
のアドレスを取得してそのtask
がcurrent
になっています。
thread_info
はespの値の下位12ビット(THREAD_SIZEによります)を0にした位置に配置されているためこのような処理でアドレスを求めることができます。
thread_info
を割り当てている部分を読めばそれが確認できそうですが今は飛ばします。
次はrq = this_rq();
を見てみます。
kernel/sched.c
static DEFINE_PER_CPU(struct runqueue, runqueues); #define this_rq() (&__get_cpu_var(runqueues))
include/asm-generic/percpu.h
/* var is in discarded region: offset to particular copy we want */ #define per_cpu(var, cpu) (*RELOC_HIDE(&per_cpu__##var, __per_cpu_offset[cpu])) #define __get_cpu_var(var) per_cpu(var, smp_processor_id())
smp_processor_id
は長いので、意味がわかりそうな部分だけ書きます。
lib/kernel_lock.c
unsigned int smp_processor_id(void) { ... int this_cpu = __smp_processor_id(); ... return this_cpu; }
細かく見ていくと難しそうですが、cpuごとにrunqueueというものが定義されていて、
this_rq
はコードを実行しているcpuのrunqueueを返しているということでしょう。
スケジューラのアルゴリズムにはあまり関係ないので、ここではcpuごとにrunqueueというものがある、
と思っておく程度でよいと思います。
prev
とrq
が終わったので次はnext
の方をみてみます。
一行だけでは意味がわからないので関係のありそうな3行を追加します。
asmlinkage void __sched schedule(void) { task_t *prev, *next; runqueue_t *rq; struct list_head *queue; int idx; ... prev = current; ... rq = this_rq(); ... array = rq->active; // ここ idx = sched_find_first_bit(array->bitmap); // ここ queue = array->queue + idx; // ここ next = list_entry(queue->next, task_t, run_list); ... prev = context_switch(rq, prev, next); ... }
コードをそのまま読むと、rq
のactive
のbitmap
から目的のidx
を探し、queue
のその位置のentryをnext
に代入しているようです。
next
の選び方は気になるのでこのあたりでrunqueue_t
の構造を把握しておきます。
大きな構造体なので使われる部分だけ抜き出します。
kernel/sched.c
#define BITMAP_SIZE ((((MAX_PRIO+1+7)/8)+sizeof(long)-1)/sizeof(long)) typedef struct prio_array prio_array_t; struct prio_array { unsigned int nr_active; unsigned long bitmap[BITMAP_SIZE]; struct list_head queue[MAX_PRIO]; }; typedef struct runqueue runqueue_t; struct runqueue { unsigned long nr_running; ... prio_array_t *active; prio_array_t *expired; prio_array_t arrays[2]; ... };
include/linux/sched.h
#define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + 40)
include/linux/list.h
struct list_head { struct list_head *next, *prev; };
list_head
はただの双方向リストです。
便利なデータ構造なので、include/linux
の下に置いてあってlinuxのコード内のいろいろなところで使わているようです。
prioというのはpriority、つまり優先度を表していて最大はMAX_PRIO
の140と定義されています。
定義をみると100という数字にも意味がありそうです。
prio_array
は優先度の数、つまり140個分のリスト(queue[MAX_PRIO]
)を持ち、
そこにリンクされたtaskの数(nr_active
)とその優先度ごとのリストにtaskが存在するかどうかを表すbitmap
をもっています。
runqueue
はactive
, expired
という2つのprio_array
へのポインタと、
それらのポインタが指し示す実際のprio_array
であるarrays[2]
があります。
active
は実行可能で、割り当てを待っているtaskのリスト、
expired
は既に実行されてある時間消費したのち、他のプロセス実行権を渡したたtaskのリストを意味します。
構造が把握できたところでidx = sched_find_first_bit(array->bitmap);
の行にあるsched_find_first_bit
の中身をみてみます。
inclide/asm-i386/bitops.h
/** * __ffs - find first bit in word. * @word: The word to search * * Undefined if no bit exists, so code should check against 0 first. */ static inline unsigned long __ffs(unsigned long word) { __asm__("bsfl %1,%0" :"=r" (word) :"rm" (word)); return word; } /* * Every architecture must define this function. It's the fastest * way of searching a 140-bit bitmap where the first 100 bits are * unlikely to be set. It's guaranteed that at least one of the 140 * bits is cleared. */ static inline int sched_find_first_bit(const unsigned long *b) { if (unlikely(b[0])) return __ffs(b[0]); if (unlikely(b[1])) return __ffs(b[1]) + 32; if (unlikely(b[2])) return __ffs(b[2]) + 64; if (b[3]) return __ffs(b[3]) + 96; return __ffs(b[4]) + 128; }
__ffs
の中で使われているbsfl
は最下位ビット位置を探す命令です。
例えば__ffs(12)
とすると12はbitで表すと1100なので3が返されます。
(bsfl
の最後のl
はデータ型がlongであるという意味なのでマニュアル等で解説を探すときは"bsf"を探しましょう)
unlikely
は、
include/linux/compiler.h
#define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0)
と定義されており、__builtin_expect
というのは、コンパイラに分岐の予測情報を与えて
ジャンプが少なくなるような最適なコードを生成させるようにするものです。
条件が真になる可能性が高いときにif(likely(..))
、 低いときにif(unlikely(...))
というようにかきます。
参考
関数名とコメントだけでもなんとなくわかりますが、つまりsched_find_first_bit
は指定されたアドレスから32*5
ビット分をみて一番初めに1になっているビットの位置を返す関数です。
コメントとunlikely
の使われ方をみると、はじめの100bitのどれかが立っている可能性は低い、ということもわかります。
続いてlist_entry
を見てみます。
include/linux/list.h
/** * list_entry - get the struct for this entry * @ptr: the &struct list_head pointer. * @type: the type of the struct this is embedded in. * @member: the name of the list_struct within the struct. */ #define list_entry(ptr, type, member) \ container_of(ptr, type, member)
include/linux/kernel.h
/** * container_of - cast a member of a structure out to the containing structure * * @ptr: the pointer to the member. * @type: the type of the container struct this is embedded in. * @member: the name of the member within the struct. * */ #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );})
include/linux/stddef.h
#undef offsetof #ifdef __compiler_offsetof #define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER) #else #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) #endif
container_of
が見づらいので具体的な引数を入れて、型も括弧もいろいろ無視するとこのように展開されます。
container_of(ptr, task_t, run_list) ↑を展開↓ __mptr = ptr; tmp = __mptr - offsetof(task_t,run_list); (task_t *)tmp;
第一引数の示すアドレスから、構造体のメンバのoffsetを引いたアドレスを返しています。
struct list_head
はそれ単体で使われるわけではなく、別の構造体のメンバとして使われるのでリストの具体的な要素を取得するときにはlist_entry
が使われます。
ここまでくればnext
への代入は読めそうです。
asmlinkage void __sched schedule(void) { task_t *prev, *next; runqueue_t *rq; struct list_head *queue; int idx; ... prev = current; ... rq = this_rq(); // 現在のcpuのrunqueue ... array = rq->active; // のactiveな方... idx = sched_find_first_bit(array->bitmap); // ...の初めのビット位置 queue = array->queue + idx; // = &(array->queue[idx]) // の優先度のqueue next = list_entry(queue->next, task_t, run_list); // の先頭の要素を次のプロセスとする ... prev = context_switch(rq, prev, next); ... }
現在のcpuのキューrq->active
のbitmap
のうち1となる初めのビット位置
を取得し(優先度の値が小さいほうが優先される)、
その次の行でその優先度のtaskのリスト(idx
位置のリスト)を取得し、
そのリストの先頭のtaskのアドレスをnextとしています。
この選び方なら、次のtaskを選ぶのにかかる時間はtaskの数に依存しないということもわかりました。
ここまでで、現在のプロセスprev
と次のプロセスnext
を選んでcontext_switch
する、というところまで書きました。
schedule
関数を読むといいつつ行数でいうと5%くらいしか書いていませんが、このペースで全部読んでまとめると量がすごいことになるので、ここで区切って気が向いたらまたどこかを読んでまとめてみます。
- 作者: Daniel P. Bovet,Marco Cesati,高橋浩和,杉田由美子,清水正明,高杉昌督,平松雅巳,安井隆宏
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2007/02/26
- メディア: 大型本
- 購入: 9人 クリック: 269回
- この商品を含むブログ (71件) を見る
- 作者: 高橋浩和,小田逸郎,山幡為佐久
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2006/11/18
- メディア: 単行本
- 購入: 14人 クリック: 197回
- この商品を含むブログ (119件) を見る