無作為ランダム化試験は臨床医学研究の分野では信頼性の高いエビデンスを確立する上で、必須となってきています。
ランダム化比較試験の統一化しようとの試みで、CONSORT statment(Consolidated Statndards of Reporting Trials)がという宣言が出されています。
以下に、そのCONSORT statmentに基づいたランダム化試験のデザインのフローチャートを示します。
しっかりとしたクライテリアの下で品質の保証された臨床研究が行われていって欲しいものです。
医療系の仕事をしています。生命の尊さ、美しさがどのようなメカニズムで生じるのかに興味があります。科学の方法論を用いて、このような問いに応えたい、私はこう思って医学生物学の基礎研究のトレーニングを受けてきました。生命を科学的手法を用いて理解を試みる上で、genomeを始めとした種々の大量データの処理が必要不可欠であることを痛感しました。また、生命科学が物理学、数学、統計学、有機化学などの種々の学問と深い関わりを持つことを実感しました。そのため、このブログは広範囲の学問領域に関しての記事を載せています。日々の学習内容を文書に書き残し、それを読み返すことによって、体系化された知識を身に付けることを目標としています。どうぞよろしくお願いします。
一つの母集団からのサンプル抽出を味わう
$ vim command.in
dat <- rnorm(10000, mean=0, sd=1)
#母集団のヒストグラムに母集団の平均値を記述(green)
png("120730_hist_1.png")
hist(dat, breaks=100)
abline(v=mean(dat), col="green")
dev.off()
#サンプルの平均値をmagendaで記述。
#サンプルの平均値のばらつきを見極める。
png("120730_hist_2.png")
hist(dat, breaks=100)
abline(v=mean(dat), col="green")
for(i in 1:10)
{
abline(v=mean(dat[sample(1000,10)], col="magenda"))
}
dev.off()
$ fg
> source("command.in")
サンプルの平均値は母平均のそばを漂っているイメージである。
この事実から言えることは、サンプル
サンプリングした標本に対しての統計学的なデータの要約方法について記述する。
この事実から言えることは、サンプル
サンプリングした標本に対しての統計学的なデータの要約方法について記述する。
#サンプリング(1000個の母集団の要素中から10個を抽出)
> sample <- dat[sample(1000,10)]
> sample
[1] 0.3197940 1.8119807 -0.2420026 -0.2343419 -0.6559658 0.5846391
[7] -0.7373418 0.5727909 0.7129338 0.0694458
#標本数
> length(sample)
[1] 10
#平均値の計算
> mean(sample)
[1] 0.2201932
#標本標準偏差の計算
#Rにはデフォルトでは不偏分散
> variance <- function(x){var(x)*(length(x)-1)/length(x)}
> sqrt(variance(sample))
[1] 0.7191055
#標本の95%信頼区域を計算(ついでに平均値が0であるかの検定も行う)
> t.test(sample, mu=0, alternative="two.sided")
One Sample t-test
data: sample
t = 0.9186, df = 9, p-value = 0.3823
alternative hypothesis: true mean is not equal to 0
95 percent confidence interval:
-0.3220500 0.7624364
sample estimates:
mean of x
0.2201932
以上より、以下の記述が可能です。> t.test(sample, mu=0, alternative="two.sided")
One Sample t-test
data: sample
t = 0.9186, df = 9, p-value = 0.3823
alternative hypothesis: true mean is not equal to 0
95 percent confidence interval:
-0.3220500 0.7624364
sample estimates:
mean of x
0.2201932
○(平均(標準偏差)[範囲])
グループA(n=20)
データ(無次元) 0.22(0.72)[-0.32 - 0.76 ]
○(平均±標準偏差)[範囲])
グループA(n=20)
データ(無次元) 0.22 ± 0.72[-0.32 - 0.76 ]
#ちなみに、95%信頼区間が0をまたいでいる[-0.32 - 0.76 ]ということは、t検定により、母平均が0とは異なるとまでは言えないということと同値である。かりに、p値が0.05を下回るようならば、95%信頼区間が正負いずれかの方向に偏ることになる。
臨床的に有意な差とは何か 〜サンプル数は多すぎても少なくてもいけない〜
2群の平均値の比較において大切なことは、「有意差が出る」ことではなくて「実質的に(ex.臨床的に、実験的に)有意な差を検出する」ことです。
例えば、A,B二つの集団の身長の平均値の差について考えます。
二群の平均値の差が0.1cm程度しかなかったとき、その差を膨大なサンプリングによって検出することに果たして意味があるのでしょうか。いやありません。
サンプル数は多すぎても問題なのです。
大切なのは、実験を行う前の段階で、どの程度の平均値の差なら実質的に有意であるかを明らかにした上で、必要最小限のサンプルを行うことです。
以下に、サンプル数とp値のシミュレーションの結果を書きます。
$ vim comman.in
result1 <- c(1:99)
for(i in 1:99)
{
#2群の平均値の差が10cmの場合
dat1 <- rnorm(mean=175.0, n=i+1, sd=5)
dat2 <- rnorm(mean=185.0, n=i+1, sd=5)
result1[i] <- t.test(dat1,dat2,alternative="two.side",mu=0, paired=FALSE,var.equal=FALSE,conf.level=0.95)$p.value
}
result2 <- c(1:99)
for(i in 1:99)
{
#2群の平均値の差が5cmの場合
dat1 <- rnorm(mean=175.0, n=i+1, sd=5)
dat2 <- rnorm(mean=180.0, n=i+1, sd=5)
result2[i] <- t.test(dat1,dat2,alternative="two.side",mu=0, paired=FALSE,var.equal=FALSE,conf.level=0.95)$p.value
}
result3 <- c(1:99)
for(i in 1:99)
{
#2群の平均値の差が1cmの場合
dat1 <- rnorm(mean=175.0, n=i+1, sd=5)
dat2 <- rnorm(mean=176.0, n=i+1, sd=5)
result3[i] <- t.test(dat1,dat2,alternative="two.side",mu=0, paired=FALSE,var.equal=FALSE,conf.level=0.95)$p.value
}
n <- c(2:100)
png("120731_n.png")
par(mfrow=c(1,3))
plot(n,result1)
plot(n,result2)
plot(n,result3)
dev.off()
$ R
> source("command.in")
平均値の差が小さくなると、統計的に有意であると判断されるのに多くのサンプルが必要であることがわかりました。
次に、平均値の差が1cmの時に、さらにサンプル数を多くして検証します。
対照群として、平均値の差がない群の比較を提示しています。
> cntrl-z
$ vim command.in.2
result4 <- c(1:999)
for(i in 1:999)
{
dat1 <- rnorm(mean=175.0, n=i+1, sd=5)
dat2 <- rnorm(mean=176.0, n=i+1, sd=5)
result4[i] <- t.test(dat1,dat2,alternative="two.side",mu=0, paired=FALSE,var.equal=FALSE,conf.level=0.95)$p.value
}
result5 <- c(1:99)
for(i in 1:999)
{
dat1 <- rnorm(mean=175.0, n=i+1, sd=5)
dat2 <- rnorm(mean=175.0, n=i+1, sd=5)
result5[i] <- t.test(dat1,dat2,alternative="two.side",mu=0, paired=FALSE,var.equal=FALSE,conf.level=0.95)$p.value
}
n <- c(2:1000)
png("120731_n_2.png")
par(mfrow=c(1,2))
plot(n,result4)
plot(n,result5)
dev.off()
$ fg
> source("command.in.2")
確かに、1000サンプル近く抽出すればそれなりに有意差が出てくるようですが、この作業に何の意味があるのでしょうか(いやない)。
あらかじめ有意であると見なすことのできる差を想定した上でその差を検出するのに必要最低限なサンプルを確保することが王道ではないかと思います。
やたらめったら、サンプルを増やしてp<0.05としたところで、その差が実際のところどの程度の影響を持つのか考える必要があろうかと思います。
一標本からのサンプルの抽出についての検討
一標本からのサンプルの抽出について検討しました。
今、身長の平均値175cm, 標準偏差が5cmの成人男性の集団(合計1000人)を考えます。
(対象の集団は175±5cm(n=1000)とも表現可)
Rを使って、この集団のデータを擬似的に作成します。
#Rの起動
$ R
#データの取得
> dat <- rnorm(mean=175, n=1000, std=5)
#データの要約を表示
> summary(dat)
Min. 1st Qu. Median Mean 3rd Qu. Max.
160.3 171.5 175.2 175.1 178.4 192.5
###解釈###
最大値が192.5cm, 最小値が160.3cm, 平均値が175.2cm
#このデータは偏りがないはずであるが、偏りがもしもある場合は、中央値と四分位範囲、範囲について議論するのがよいとされる。
第一四分位数が171.5cm, 第三分位数が178.4cm(四分位範囲:171.5-178.4cm)
(中央値(四分位範囲)[範囲]): 175.2(171.5 - 178.4)[160.3 - 192.5] cm
ここで、この集団から無作為に10人抽出して身長を測定することを考えます。
この作業から、母集団の平均値をどの程度推定できるか検討してみます。
#ランダムに50人のサンプル抽出
> sample <- dat[sample(1000,10)]
今、身長の平均値175cm, 標準偏差が5cmの成人男性の集団(合計1000人)を考えます。
(対象の集団は175±5cm(n=1000)とも表現可)
Rを使って、この集団のデータを擬似的に作成します。
#Rの起動
$ R
#データの取得
> dat <- rnorm(mean=175, n=1000, std=5)
#データの要約を表示
> summary(dat)
Min. 1st Qu. Median Mean 3rd Qu. Max.
160.3 171.5 175.2 175.1 178.4 192.5
###解釈###
最大値が192.5cm, 最小値が160.3cm, 平均値が175.2cm
#このデータは偏りがないはずであるが、偏りがもしもある場合は、中央値と四分位範囲、範囲について議論するのがよいとされる。
第一四分位数が171.5cm, 第三分位数が178.4cm(四分位範囲:171.5-178.4cm)
(中央値(四分位範囲)[範囲]): 175.2(171.5 - 178.4)[160.3 - 192.5] cm
ここで、この集団から無作為に10人抽出して身長を測定することを考えます。
この作業から、母集団の平均値をどの程度推定できるか検討してみます。
#ランダムに50人のサンプル抽出
> sample <- dat[sample(1000,10)]
#標本平均を算出
> mean
[1] 174.9052
#ヒストグラムに母平均値を示す垂線を重ねて描写
png("120730_hist_1.png")
hist(dat, breaks=100)
abline(v=mean(dat), col="green")
dev.off()
#繰り返し標本抽出と標本平均の算出を行う。
> mean(dat[sample(1000,10)])
[1] 175.2295
> mean(dat[sample(1000,10)])
[1] 173.3754
> mean(dat[sample(1000,10)])
[1] 179.5982
> mean(dat[sample(1000,10)])
[1] 177.0066
> mean(dat[sample(1000,10)])
[1] 173.4215
#ヒストグラムの上に標本平均の垂線を重ねて描写(計5回のサンプリング)
png("120730_hist_2.png")
hist(dat, breaks=100)
abline(v=mean(dat), col="green")
abline(v=mean(dat[sample(1000,10)], col="magenda"))
abline(v=mean(dat[sample(1000,10)], col="magenda"))
abline(v=mean(dat[sample(1000,10)], col="magenda"))
abline(v=mean(dat[sample(1000,10)], col="magenda"))
abline(v=mean(dat[sample(1000,10)], col="magenda"))
dev.off()
sampleオブジェクト(対象の母集団から50人の標本抽出を行ったもの)の平均値が、175と比べて統計的に有意に異なるかを検定する。(一標本t検定)
> t.test(sample, mu=175, alternative="two.sided")
One Sample t-test
data: sample
t = -0.1224, df = 49, p-value = 0.9031
alternative hypothesis: true mean is not equal to 175
95 percent confidence interval:
173.3477 176.4626
sample estimates:
mean of x
174.9052
この結果、p=0.9031<0.05であり、統計的に有意に母集団の平均が175cmと異なるとは言えない。(母平均が175cmであるとは言えないが。)
そして、母平均は173.3477cmから176.4626cmの間に95%の確率で存在する。
(95%信頼区域: 173.3477 - 176.4626 cm)
Cent OSサーバーにRをインストールする
Cent OSはyumでRをインストールができません。
そのため、サードパーティーを利用してRをインストールしてあげる必要があります。
epelというサードパーティーリポジトリにRが収録されているそうです。
今日はそのための方法を書きます。
#OSのバージョンを調べる
$ cat /etc/redhat-release
CentOS release 5.7 (Final)
#最後はepelレポジを無効化しておきます。
#サードパーティーは必要に応じてつか
$ sudo vim /etc/yum.repos.d/epel.repo
1 [epel]
2 name=Extra Packages for Enterprise Linux 5 - $basearch
3 #baseurl=http://download.fedoraproject.org/pub/epel/5/$basearch
4 mirrorlist=http://mirrors.fedoraproject.org/mirrorlist?repo=epel-5&arch=$basearch
5 failovermethod=priority
6 enabled=1 ########ここを0に変更#########
7 gpgcheck=1
8 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL
そのため、サードパーティーを利用してRをインストールしてあげる必要があります。
epelというサードパーティーリポジトリにRが収録されているそうです。
今日はそのための方法を書きます。
#OSのバージョンを調べる
$ cat /etc/redhat-release
CentOS release 5.7 (Final)
#管理者権限で、epelリポジトリを登録する
$ sudo rpm -Uvh http://dl.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm
#epelレポジトリの追加を確認する
$ sudo yum repolist
repo id repo name status
CRAN http://cran.md.tsukuba.ac.jp/ 5
base CentOS-5 - Base 3,591
epel Extra Packages for Enterprise Linux 5 - x86_64 7,107
extras CentOS-5 - Extras 273
updates CentOS-5 - Updates 770
#あとはyumを使ってインストールするだけ
$ sudo yum install R
#最後はepelレポジを無効化しておきます。
#サードパーティーは必要に応じてつか
$ sudo vim /etc/yum.repos.d/epel.repo
1 [epel]
2 name=Extra Packages for Enterprise Linux 5 - $basearch
3 #baseurl=http://download.fedoraproject.org/pub/epel/5/$basearch
4 mirrorlist=http://mirrors.fedoraproject.org/mirrorlist?repo=epel-5&arch=$basearch
5 failovermethod=priority
6 enabled=1 ########ここを0に変更#########
7 gpgcheck=1
8 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL
#以下のオプションをつければ、epelを無効化している状態でも必要に応じて、使用することができる。
–enablerepo=epel
#epelレポジトリの無効化を確認する
$ sudo yum repolist
repo id repo name status
CRAN http://cran.md.tsukuba.ac.jp/ 5
base CentOS-5 - Base 3,591
extras CentOS-5 - Extras 273
updates CentOS-5 - Updates 770
#Rの動作をチェックします。
$ R --version
R version 2.15.1 (2012-06-22) -- "Roasted Marshmallows"
Copyright (C) 2012 The R Foundation for Statistical Computing
#つい最近のバージョンが入っていることがわかりました。
#起動します。
$ R
R version 2.15.1 (2012-06-22) -- "Roasted Marshmallows"
Copyright (C) 2012 The R Foundation for Statistical Computing
ISBN 3-900051-07-0
Platform: x86_64-redhat-linux-gnu (64-bit)
Rは、自由なソフトウェアであり、「完全に無保証」です。
一定の条件に従えば、自由にこれを再配布することができます。
配布条件の詳細に関しては、'license()'あるいは'licence()'と入力してください。
Rは多くの貢献者による共同プロジェクトです。
詳しくは'contributors()'と入力してください。
また、RやRのパッケージを出版物で引用する際の形式については
'citation()'と入力してください。
'demo()'と入力すればデモをみることができます。
'help()'とすればオンラインヘルプが出ます。
'help.start()'でHTMLブラウザによるヘルプがみられます。
'q()'と入力すればRを終了します。
#うまくいきました。
アドレス空間の旅
メモリ確保の方法によって確保される領域の番地に差異が生じるかを検討しました。
// array_address.c
#include<stdio.h> // printf
#include<stdlib.h> // malloc, free
#define LENGTH 3
int main(void)
{
//配列の宣言と同時に、特定の大きさだけ仮想アドレス空間内に確保している。
char array_static_1[LENGTH];
char array_static_2[LENGTH];
char *array_dynamic_ptr_1;
char *array_dynamic_ptr_2;
int i;
/*mallocにより(int型の大きさ)× LENGTHだけの値を与える。
戻り値は、mallcにより確保された領域の先頭のアドレスである/。
*/
array_dynamic_ptr_1 = (char *) malloc (sizeof (char) * LENGTH);
array_dynamic_ptr_2 = (char *) malloc (sizeof (char) * LENGTH);
for(i = 0; i<LENGTH; i++)
printf("array_static_1[%d]=%p\n", i, &array_static_1[i]);
for(i = 0; i<LENGTH; i++)
printf("array_static_2[%d]=%p\n", i, &array_static_2[i]);
for(i = 0; i<LENGTH; i++)
printf("array_dynamic_ptr_1[%d]=%p\n", i, &array_dynamic_ptr_1[i]);
for(i = 0; i<LENGTH; i++)
printf("array_dynamic_ptr_2[%d]=%p\n", i, &array_dynamic_ptr_2[i]);
//確保したメモリ領域を開放。
free(array_dynamic_ptr_1);
free(array_dynamic_ptr_2);
return 0;
}
$ gcc -o array_address array_address.c
$ ./array_address
array_static_1[0]=0xbf8b592d
array_static_1[1]=0xbf8b592e
array_static_1[2]=0xbf8b592f
array_static_2[0]=0xbf8b592a
array_static_2[1]=0xbf8b592b
array_static_2[2]=0xbf8b592c
array_dynamic_ptr_1[0]=0x80dd008
array_dynamic_ptr_1[1]=0x80dd009
array_dynamic_ptr_1[2]=0x80dd00a
array_dynamic_ptr_2[0]=0x80dd018
array_dynamic_ptr_2[1]=0x80dd019
array_dynamic_ptr_2[2]=0x80dd01a
この結果から大変興味深いことがわかります。
それは、mallocで確保される領域の方が、静的に割り当てられる領域の方がアドレス番地が若いことがわかります。そして、同じカテゴリの変数は、連続して割り当てられていることがわかります。
このことを理解するには、仮想アドレス空間の概念を理解する必要があるようです。
UNIXの場合、プログラムが実行されると、各プロセスごとに、
32bitOSの場合、0x00000000-0xffffffff番地までの連続した合計32bit(=4GB)のアドレス空間が割り当てられるとのことです。
もちろん、このアドレス空間というのは仮想メモリのことであり、実メモリとはまったく異なります。OSがリソース管理のために提供するサービスだと考えればよいです。
そして、mallcにより確保される領域は、データ領域とよばれる所であり、それは比較的若い番地が割り当てられています。
一方、静的に確保された領域は、スッタク領域と呼ばれ、比較的大きい番地に割り当てられています。スタック領域は、大きい番地から小さい番地に向けて変数が確保されていくのが特徴的です。
連続するアドレスをいかに確保するか
配列を扱うために、仮想アドレス空間に特定の領域を確保するときは、一般に配列を宣言するときに同時に静的に確保する方法と、mallcを用いて動的に確保し、freeで開放する方法の二つが存在します。
今回は、これら二つの方法を用いたプログラムを検討しました。
// array_static.c
#include<stdio.h> // printf
#define LENGTH 10
int main(void)
{
//配列の宣言と同時に、特定の大きさだけ仮想アドレス空間内に確保している。
int array[LENGTH];
int i;
for(i =0; i<LENGTH; i++)
{
array[i] = i * 3;
}
for(i = 0; i<LENGTH; i++)
{
printf("array[%d]=%d\n", i, array[i]);
}
}
// array_dynamic.c
#include<stdio.h> // printf
#include<stdlib.h> // malloc
#define LENGTH 10
int main(void){
int *array;
int i;
/*mallocにより(int型の大きさ)× LENGTHだけの値を与える。
戻り値は、mallcにより確保された領域の先頭のアドレスである。
*/
array = (int *) malloc (sizeof (int) * LENGTH);
for(i = 0; i < LENGTH; i++)
{
array[i] = i * 3;
}
for(i = 0; i < LENGTH; i++)
{
printf("array[%d]=%d\n", i, array[i]);
}
//確保したメモリ領域を開放。
free(array);
return 0;
}
変数のスコープを検討する。
変数を考える上で大切となってくる概念のひとつが、「スコープ」です。
今回は、内部変数、大域変数、静的変数について検討します。
// scope.c
#include<stdio.h>
//以下、内部変数、大域変数、静的変数を宣言する。
//初期値は各々0に設定し、forループが回ると値がどう変化するかを調査する。
/*大域宣言(z):関数の外で宣言され、すべての関数で有効な変数*/
int z=0;
int main(void)
{
int i;
for(i=1;i<11; i++)
{
printf("---%d times---\n",i);
myfunc();
}
return 0;
}
int myfunc(){
/*静的宣言(x):宣言された関数の内部でのみ有効であるが、
その関数から抜けても消滅しない変数。もう一度同じ関数が呼ばれたときに、
前回の関数実行で保存された値を引き続きしようすることができる。*/
static int x = 0;
/*内部宣言(y):宣言された関数の内部でのみ有効な変数*/
int y=0;
x = x+1;
y = y+1;
z = z+1;
printf("x=%d\t y=%d\t z=%d\n",x,y,z);
}
$ gcc -o scope scope.c
$ ./scope
---1 times---
x=1 y=1 z=1
---2 times---
x=2 y=1 z=2
---3 times---
x=3 y=1 z=3
---4 times---
x=4 y=1 z=4
---5 times---
x=5 y=1 z=5
---6 times---
x=6 y=1 z=6
---7 times---
x=7 y=1 z=7
---8 times---
x=8 y=1 z=8
---9 times---
x=9 y=1 z=9
---10 times---
x=10 y=1 z=10
内部変数yはループのたびに1がセットされるのに対して、
大域変数zや静的変数xはセットされた値が保持されるため、1ずつインクリメントされていくのである。
低水準入出力と高水準入出力の速度の違い
低水準入出力関数(システムコールのwrite, read)は、呼び出しがとても遅いのでprintfなどの高水準のブッファリングを取り入れた標準関数を使用することが一般的に推奨されています。
今回は、低水準、高水準の両者で同様の処理をしたときにどの程度、所要時間に差が出るのかを検証するプログラムを勉強しました。
下に、二つのプログラムをのせますが、どちらも二つのファイルをオープンして一方から内容をバイト単位で読み込み、他方へバイト単位で書き出すプログラムです。
入出力処理の前後で、現在時刻を取得し、その差分で所要時間を計算させています。
○システムコールを使うタイプ
// high_level_io.c
#include<stdio.h> //fprintf
#include<stdlib.h>//exit
#include<sys/time.h> //gettimeofday
#include<sys/types.h>//read
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
double time_pass()
{
struct timeval tv;
//現在の時刻を取得
gettimeofday(&tv, NULL);
//秒数に換算して値を返却
return tv.tv_sec + (double) tv.tv_usec * 1e-6;
}
int main(int argc, char *argv[])
{
int *fdin, *fdout;
double start_time, end_time;
char c;
if(argc != 3)
{
fprintf(stderr, "Usage: copy_highio <file1> <file2>\n");
exit(EXIT_FAILURE);
}
//入力ファイルを開く
if((fdin = open(argv[1], O_RDONLY)) < 0)
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//出力ファイルを開く
if((fdout = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0)
{
perror(argv[2]);
exit(EXIT_FAILURE);
}
//開始時刻を取得
start_time = time_pass();
//ファイルよりバイト単位でデータを読み込んで、出力ファイルへ出力
while(read(fdin, &c, 1) == 1)
{
write(fdout, &c, 1);
}
//終了時刻を取得
end_time = time_pass();
//ファイルを閉じる。
close(fdin);
close(fdout);
printf("%.4f\n", end_time - start_time);
}
○標準入出力を使うタイプ
// high_level_io.c
#include<stdio.h> //fprintf
#include<stdlib.h>//exit
#include<sys/time.h> //gettimeofday
double time_pass()
{
struct timeval tv;
//現在の時刻を取得
gettimeofday(&tv, NULL);
//秒数に換算して値を返却
return tv.tv_sec + (double) tv.tv_usec * 1e-6;
}
int main(int argc, char *argv[])
{
FILE *fpin, *fpout;
double start_time, end_time;
char c;
if(argc != 3)
{
fprintf(stderr, "Usage: copy_highio <file1> <file2>\n");
exit(EXIT_FAILURE);
}
//入力ファイルを開く
if((fpin = fopen(argv[1], "rb")) == NULL)
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//出力ファイルを開く
if((fpout = fopen(argv[2], "wb")) == NULL)
{
perror(argv[2]);
exit(EXIT_FAILURE);
}
//開始時刻を取得
start_time = time_pass();
//ファイルよりバイト単位でデータを読み込んで、出力ファイルへ出力
while(fread(&c, 1, 1, fpin) == 1)
{
fwrite(&c, 1, 1,fpout);
}
//終了時刻を取得
end_time = time_pass();
//ファイルを閉じる。
fclose(fpin);
fclose(fpout);
printf("%.4f\n", end_time - start_time);
}
#プログラムのビルド
$ gcc -o low_level_io low_level_io.c
$ gcc -o high_level_io high_level_io.c
#GNUが配布しているlsコマンドのソースコードをコピーしてみます
$ ./low_level_io ls.c out.2
0.8041
$ ./high_level_io ls.c out.1
0.0282
今回は、低水準、高水準の両者で同様の処理をしたときにどの程度、所要時間に差が出るのかを検証するプログラムを勉強しました。
下に、二つのプログラムをのせますが、どちらも二つのファイルをオープンして一方から内容をバイト単位で読み込み、他方へバイト単位で書き出すプログラムです。
入出力処理の前後で、現在時刻を取得し、その差分で所要時間を計算させています。
○システムコールを使うタイプ
// high_level_io.c
#include<stdio.h> //fprintf
#include<stdlib.h>//exit
#include<sys/time.h> //gettimeofday
#include<sys/types.h>//read
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
double time_pass()
{
struct timeval tv;
//現在の時刻を取得
gettimeofday(&tv, NULL);
//秒数に換算して値を返却
return tv.tv_sec + (double) tv.tv_usec * 1e-6;
}
int main(int argc, char *argv[])
{
int *fdin, *fdout;
double start_time, end_time;
char c;
if(argc != 3)
{
fprintf(stderr, "Usage: copy_highio <file1> <file2>\n");
exit(EXIT_FAILURE);
}
//入力ファイルを開く
if((fdin = open(argv[1], O_RDONLY)) < 0)
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//出力ファイルを開く
if((fdout = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0)
{
perror(argv[2]);
exit(EXIT_FAILURE);
}
//開始時刻を取得
start_time = time_pass();
//ファイルよりバイト単位でデータを読み込んで、出力ファイルへ出力
while(read(fdin, &c, 1) == 1)
{
write(fdout, &c, 1);
}
//終了時刻を取得
end_time = time_pass();
//ファイルを閉じる。
close(fdin);
close(fdout);
printf("%.4f\n", end_time - start_time);
}
○標準入出力を使うタイプ
// high_level_io.c
#include<stdio.h> //fprintf
#include<stdlib.h>//exit
#include<sys/time.h> //gettimeofday
double time_pass()
{
struct timeval tv;
//現在の時刻を取得
gettimeofday(&tv, NULL);
//秒数に換算して値を返却
return tv.tv_sec + (double) tv.tv_usec * 1e-6;
}
int main(int argc, char *argv[])
{
FILE *fpin, *fpout;
double start_time, end_time;
char c;
if(argc != 3)
{
fprintf(stderr, "Usage: copy_highio <file1> <file2>\n");
exit(EXIT_FAILURE);
}
//入力ファイルを開く
if((fpin = fopen(argv[1], "rb")) == NULL)
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//出力ファイルを開く
if((fpout = fopen(argv[2], "wb")) == NULL)
{
perror(argv[2]);
exit(EXIT_FAILURE);
}
//開始時刻を取得
start_time = time_pass();
//ファイルよりバイト単位でデータを読み込んで、出力ファイルへ出力
while(fread(&c, 1, 1, fpin) == 1)
{
fwrite(&c, 1, 1,fpout);
}
//終了時刻を取得
end_time = time_pass();
//ファイルを閉じる。
fclose(fpin);
fclose(fpout);
printf("%.4f\n", end_time - start_time);
}
#プログラムのビルド
$ gcc -o low_level_io low_level_io.c
$ gcc -o high_level_io high_level_io.c
#GNUが配布しているlsコマンドのソースコードをコピーしてみます
$ ./low_level_io ls.c out.2
0.8041
$ ./high_level_io ls.c out.1
0.0282
0.8041秒と、0.0282秒とは。。。。
ブッファリングを導入したことで飛躍的に処理が早くなっていることが一目両全です。
システムコールはやはり特別なものなんですね。
バイナリファイルの扱い
バイナリファイルの扱いについて勉強しました。
バイナリファイル中のイメージは、文字列に変換されていない”文字列”がバイナリ形式で並んでいる感じです。
// write_binary.c
#include<stdio.h> //perror
#include<stdlib.h> //exit
int main(void)
{
FILE *fp;
char string[] = "We are the world.";
char string2[] = "penguin";
//ファイルをwb(書き込み、バイナリモード)で開く
if((fp = fopen("out.bin", "wb")) == NULL)
{
perror("out.bin");
exit(EXIT_FAILURE);
}
//fpが参照するファイルにstringの参照する内容を書き込む
fwrite(string,sizeof(char), sizeof(string), fp);
fwrite(string2,sizeof(char), sizeof(string2), fp);
if(fclose(fp) == EOF)
{
perror("out.bin");
exit(EXIT_FAILURE);
}
return 0;
}
テキストファイルで保存する場合、 データをすべてテキストに変換する必要が出るため処理に時間がかかったり、サイズが大きくなってしまうというデメリットがあります。
そこで、メモリ中のデータをテキストに置き換えずにそのまま書き出すバイナリ形式のファイルが重宝されるようです。
プログラムを書く上でのポイントは、fopenをb(binary)モードで行うことです。
バイナリファイル中のイメージは、文字列に変換されていない”文字列”がバイナリ形式で並んでいる感じです。
// write_binary.c
#include<stdio.h> //perror
#include<stdlib.h> //exit
int main(void)
{
FILE *fp;
char string[] = "We are the world.";
char string2[] = "penguin";
//ファイルをwb(書き込み、バイナリモード)で開く
if((fp = fopen("out.bin", "wb")) == NULL)
{
perror("out.bin");
exit(EXIT_FAILURE);
}
//fpが参照するファイルにstringの参照する内容を書き込む
fwrite(string,sizeof(char), sizeof(string), fp);
fwrite(string2,sizeof(char), sizeof(string2), fp);
if(fclose(fp) == EOF)
{
perror("out.bin");
exit(EXIT_FAILURE);
}
return 0;
}
// read_binary.c
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
FILE *fp;
char string[18];
char string2[8];
if((fp = fopen("out.bin", "rb")) ==NULL)
{
perror("out.bin");
exit(EXIT_FAILURE);
}
if(fread(string , sizeof(char), 18, fp)!=18)
{
perror("read_binary");
exit(EXIT_FAILURE);
}
if(fread(string2, sizeof(char), 8, fp)!=8)
{
perror("read_binary");
exit(EXIT_FAILURE);
}
if(fclose(fp)==EOF)
{
perror("read_binary");
exit(EXIT_FAILURE);
}
printf("%s%s\n", string, string2);
return 0;
}
$ gcc -o write_binary.c
$ gcc -o read_binary.c
#write_binaryによって生成されたファイルはlessで開くと文字化けしてしまう。
$ less out.bin
We are the world.^@penguin^@
#rea_binaryならちゃんと読み込むことができる。
$ ./read_binary
We are the world.penguin
ライブラリ関数fgetsを用いてファイルから一行ずつ文を読み込む
ライブラリ関数のfgetsはストリームから一行読み込んで第1引数のバッファに格納する。
SYNOPSIS
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
以下のプログラムはこれを用いて、ファイルをコピーを実装したもの。
/* mycopy */
#include<stdio.h> //printf, perror
#include<stdlib.h> //EXIT_FAILURE, exit
#define BUFSIZE 1024
int main(int argc, char *argv[])
{
FILE *in, *out;
//bufferを用意
char buf[BUFSIZE];
//エラー処理
//コマンドライン引数が2以下の時はエラー
if(argc < 3)
{
fprintf(stderr, "Usage : copy <file1> <file2>\n");
exit(EXIT_FAILURE);
}
//コピー元を読み込みモードで開く
if((in = fopen(argv[2],"r"))==NULL)
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//コピー先を読み込みモードで開く
//wはファイルが無い場合に新規作成されるモード
if((in = fopen(argv[2],"w"))==NULL)
{
perror(argv[2]);
exit(EXIT_FAILURE);
}
//一行ずつファイルからbufへ読み込む
//文の末尾を示す'\0'出会ったり、BUFSIZE-1だけ読み込む
//BUSIZE-1だけしか読み込まないのは'\0'の文が予め必要だから。
while(fgets(buf, BUFSIZE-1, in) != NULL)
{
//コピー先に書き込む
fputs(buf, out);
}
//コピー元を閉じる
if(EOF == fclose(in))
{
perror(argv[1]);
exit(EXIT_FAILURE);
}
//コピー先を閉じる
if(EOF == fclose(out))
{
perror(argv[2]);
exit(EXIT_FAILURE);
}
return 0;
}
$ gcc -o mycopy mycopy.c
$ ./mycopy
$ ls
mycopy mycopy.c
$ echo "We are the world." > file.in
$ cat file.in
We are the world.
$ ls
file.in mycopy mycopy.c
$ rm file.in
$ ls
mycopy mycopy.c
$ ./mycopy
Usage : copy <file1> <file2>
$ echo "We are the world." > file.in
$ ls
file.in mycopy mycopy.c
$ cat file.in
We are the world.
$ ./mycopy file.in
Usage : copy <file1> <file2>
$ ls
file.in mycopy mycopy.c
$ ./mycopy file.in file.out
$ ls
file.in file.out mycopy mycopy.c
$ cat file.out
We are the world.
マクロEXIT_FAILURE
マクロ EXIT_FAILUREは、エラー処理の中で、exit()の引数として与えられることが多いかと思います。
今回は、EXIT_FAILURE及びEXIT_SUCCESSの定義を見ました。
$ /usr/include/stdlib.h
#define EXIT_FAILURE 1 /* Failing exit status. */
#define EXIT_SUCCESS 0 /* Successful exit status. */
今回は、EXIT_FAILURE及びEXIT_SUCCESSの定義を見ました。
$ /usr/include/stdlib.h
#define EXIT_FAILURE 1 /* Failing exit status. */
#define EXIT_SUCCESS 0 /* Successful exit status. */
なるほど。
exit(EXIT_FAILURE)はexit(1)と同義になるわけですね。
atoiの使い方
atoiはancii to integerの略だそうです。
Cの標準ライブラリの関数です。
/* add.c */
#include<stdio.h> // printf
#include<stdlib.h>// atoi
int main(int argc, int *argv[])
{
int a, b, c;
//ascii to integer
//文字列をint型の変数に変換する
// convert a string to an integer
a = atoi(argv[1]);
b = atoi(argv[2]);
c = a + b;
printf("%d + %d = %d\n", a,b,c);
return 0;
}
Cの標準ライブラリの関数です。
/* add.c */
#include<stdio.h> // printf
#include<stdlib.h>// atoi
int main(int argc, int *argv[])
{
int a, b, c;
//ascii to integer
//文字列をint型の変数に変換する
// convert a string to an integer
a = atoi(argv[1]);
b = atoi(argv[2]);
c = a + b;
printf("%d + %d = %d\n", a,b,c);
return 0;
}
$ gcc -o add add.c
$ ./add 1 2
1 + 2 = 3
makefileの基本
makefileはソースコードを複数に分割して開発を行うときに重宝されます。
ファイル間の依存関係を維持しながら、部分的にコンパイルを繰り返すことができるからです。
/* main.c */
#include"myheader.h"
//外部ファイルに内容は記述
extern void aisatsu(char *string);
int main()
{
aisatsu("Hello.\n");
return 0;
}
/* myheader.h*/
#include<stdio.h>
#include"myheader.h"
void aisatsu(char *string)
{
//引数が参照する文字列を標準出力へ表示する
printf("%s", string);
}
ファイル間の依存関係を維持しながら、部分的にコンパイルを繰り返すことができるからです。
/* main.c */
#include"myheader.h"
//外部ファイルに内容は記述
extern void aisatsu(char *string);
int main()
{
aisatsu("Hello.\n");
return 0;
}
/* myheader.h*/
#include<stdio.h>
#include"myheader.h"
void aisatsu(char *string)
{
//引数が参照する文字列を標準出力へ表示する
printf("%s", string);
}
/* myheader.h */
// nothing
以下はmakefileです。
#makefile
# compiler
CC=gcc
# Where to install
INSTDIR=/usr/local/bin
# Where are include files kept
INCLUDE=.
# Options for development
CFLAGS=-g -Wall -ansi
aisatsu: main.o a.o
$(CC) -o aisatsu main.o a.o
main.o: main.c myheader.h
$(CC) -I$(INCLUDE) -c main.c
a.o: a.c myheader.h
$(CC) -I$(INCLUDE) -c a.c
clean:
-rm main.o a.o
install: aisatsu
@cp aisatsu $(INSTDIR);
@chmod a+x $(INSTDIR)/aisatsu;
@echo "Installed in $(INSTDIR)"
#makeを実行
$ make
gcc -I. -c main.c
gcc -I. -c a.c
gcc -o aisatsu main.o a.o
#オブジェクトファイルのお掃除
$ make clean
rm main.o a.o
#インストール
#管理者権限の必要なコマンドが含まれるため怒られる
$ make install
gcc -I. -c main.c
gcc -I. -c a.c
gcc -o aisatsu main.o a.o
cp: 通常のファイル `/usr/local/bin/aisatsu' を作成できません: Permission denied
make: *** [install] エラー 1
#管理者権限で実行
$ sudo make install
[sudo] password for yumiko:
Installed in /usr/local/bin
#コマンドを実行
#/usr/local/binにはパスがデフォルトで通っているため、実行ファイル名だけを入力すればよい。
$ aisatsu
Hello.
time()の値から現在の日付を計算
ライブラリ関数のgmtime()を用いれば、timeの結果から現在の日付を算出することが可能です。
$ man gmtime
#include<time.h>
struct tm *gmtime(const time_t *timep);
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
上に示すtm型の構造体を返却してくるようである。
/* mytime.c */
//低水準の時刻から現在の時刻を計算し、表示させるプログラム
#include<stdio.h> //printf
#include<time.h> //time, gmtime
int main(void)
{
//UNIXエポック(1970.01.01.AP00:00)を起点とした時間(second)を表示
time_t mytime;
struct tm *time_ptr;
time(&mytime);
//gmtimeより現在の日付を算出
time_ptr = gmtime(&mytime);
printf("Raw time is %ld\n", mytime);
printf("date : %02d/%02d/%02d\n", time_ptr->tm_year, time_ptr->tm_mon+1, time_ptr->tm_mday);
return 0;
}
$ ./mytime2
Raw time is 1342782353
date : 112/07/20
$ man gmtime
#include<time.h>
struct tm *gmtime(const time_t *timep);
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
上に示すtm型の構造体を返却してくるようである。
/* mytime.c */
//低水準の時刻から現在の時刻を計算し、表示させるプログラム
#include<stdio.h> //printf
#include<time.h> //time, gmtime
int main(void)
{
//UNIXエポック(1970.01.01.AP00:00)を起点とした時間(second)を表示
time_t mytime;
struct tm *time_ptr;
time(&mytime);
//gmtimeより現在の日付を算出
time_ptr = gmtime(&mytime);
printf("Raw time is %ld\n", mytime);
printf("date : %02d/%02d/%02d\n", time_ptr->tm_year, time_ptr->tm_mon+1, time_ptr->tm_mday);
return 0;
}
$ ./mytime2
Raw time is 1342782353
date : 112/07/20
112年って。。。。2000年問題が起こっています笑
UNIXエポックからの経過時間
time関数はシステムコールの一つです。
//UNIXエポック(1970.01.01.AP00:00)を起点とした時間を秒を単位として返します。
$ man 2 time
SYNOPSIS
#include <time.h>
time_t time(time_t *t);
DESCRIPTION
time() returns the time since the Epoch (00:00:00 UTC, January 1, 1970), measured in seconds.
/* mytime.c */
//低水準の時刻を表示するプログラム
#include<stdio.h> //printf
#include<time.h> //time
int main(void)
{
//UNIXエポック(1970.01.01.AP00:00)を起点とした時間(second)を表示
time_t mytime;
time(&mytime);
printf("The time is %ld\n", mytime);
//UNIXエポックからの経過時間を自分で計算する
int mytime2;
mytime2 = (2012-1970) * 365 * 24 * 60 * 60;
printf("The time is %d\n", mytime2);
return 0;
}
$ ./mytime
The time is 1342781355
The time is 1324512000
Advanced Programming in the UNIX Environmentのヘッダーファイルをダウンロードする
Advanced Programming in the UNIX Environmentのフォローアップサイト
http://www.yendor.com/programming/unix/apue/apue.html
#Linuxならwget
$ wget http://www.yendor.com/programming/unix/apue/lib.svr4/ourhdr.h
#Mac OS Xならcurlコマンドを使う。-Oオプションが必要
$ curl -O http://www.yendor.com/programming/unix/apue/lib.svr4/ourhdr.h
#中身を見る
$ less ourhdr.h
http://www.yendor.com/programming/unix/apue/apue.html
#Linuxならwget
$ wget http://www.yendor.com/programming/unix/apue/lib.svr4/ourhdr.h
#Mac OS Xならcurlコマンドを使う。-Oオプションが必要
$ curl -O http://www.yendor.com/programming/unix/apue/lib.svr4/ourhdr.h
#中身を見る
$ less ourhdr.h
bashが親プロセスであることを確かめるプログラム
ターミナルでプログラムを呼び出すときに、親プロセスとなるは当然shell(bash)です。
シェルは内部でfork()を用いて、プロセスを複製し、その子プロセスにおいて新規のプロセスを呼び出しているのです。
今日は、システムコールgetppid()を用いて、親プロセス(すなわちbash)のプロセスIDを取得するプログラムを書きました。
$ vim parent_process.c
/* parent_process.c */
#include<stdio.h> //printf
#include <sys/types.h> //getppid, getpid
#include <unistd.h> //getppid, getpid
int main(void)
{
printf("My process ID is %d\n", getpid());
printf("My parent PID is %d\n", getppid());
return 0;
}
#コンパイル
$ gcc -o parent_process parent_process.c
#現在、動いているPIDを表示
$ ps
PID TTY TIME CMD
3657 pts/0 00:00:00 bash
3702 pts/0 00:00:00 less
4973 pts/0 00:00:00 ps
$ ./parent_process
My process ID is 4983
My parent PID is 3657
親、子それぞれのプロセスID
このブログでforkの基本的な使用方法について幾度か書いたことがあります。
今回は、親、子各々のプロセスから自分の環境におけるプロセスIDを表示させるプログラムを作りました。
fork()というシステムコールはまったく同じプロセスを複製するわけですが、その際に親と子でもで戻り値が異なることが最大のミソです。
戻り値をチェックして、if文を使って親と子それぞれの下流の処理を記述してあげればいいのです。
// fork_test.c
#include<sys/types.h> //fork
#include<stdio.h> //printf
#include<stdlib.h> //exit
int main(void)
{
int pid;
if((pid = fork()) < 0){
perror("fork_test");
exit(1);
}
else if((pid == 0)){
//child processには0が戻る
printf("I am the child process.My process ID is %d\n", pid);
return 0;
}
else{
//parent processには子のプロセスIDが戻る
printf("I am the parent process.My process ID is %d\n", pid);
}
return 0;
}
$ gcc -o fork_test fork_test.c
$ ./fork_test
I am the parent process.My process ID is 3412
I am the child process.My process ID is 0
少しく工夫して、親、子のプロセス各々でシステムコールのgetpidを用いて、自らのPIDを取得させるとともに、if文の中で実行されたforkの戻り値を表示させるプログラムを作りました。
子プロセスではさらに、getppid(get parent process id)というシステムコールにより、親のPIDを取得し、表示させています。
// fork_test_2.c
#include<sys/types.h> //fork
#include<stdio.h> //printf
#include<stdlib.h> //exit
#include <sys/types.h> //getpid, getppid
#include <unistd.h> //getpid, getppid
int main(void)
{
int pid;
if((pid = fork()) < 0){
perror("fork_test");
exit(1);
}
else if((pid == 0)){
//getpid()によりProcess IDを取得、表示
printf("I am the child process.My process ID is %d\n", getpid());
//child processには0が戻る
printf("My pid variance is %d in my environment.\n", pid);
//getppidにて、親プロセスのprocess IDを調べる。
printf("My parent process ID is %d\n", getppid());
return 0;
}
else{
//getpid()によりProcess IDを取得、表示
printf("I am the parent process.My process ID is %d\n", getpid());
//parent processにが子プロセスProcess IDが戻る
printf("My pid variance is %d in my environment.\n", pid);
}
return 0;
}
ファイル記述子、ファイル型、ファイルポインタ、stdin、stdout、stderrについての考察
C言語の文法的な意味においては
ファイル記述子はファイルポインタが参照する構造体(ファイルシステムのiノードリストに含まれる利用者ファイル記述子表の構造体)の持つ要素のうちの一つ(int _fileno)です。
FILE型の定義を調べてみます。
$ less -N /usr/include/stdio.h
48 /* The opaque type of streams. This is the definition used elsewhere. */
49 typedef struct _IO_FILE FILE;
なるほど。48-49行目のところでFILE型の実体は、_IO_FILEという構造体であることがわかります。
_IO_FILEは、stdio.hがインクルードしているヘッダファイルの/usr/include/libio.hに記述されています。それを見てみます。
$ less -N /usr/include/libio.h
271 struct _IO_FILE {
272 int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
273 #define _IO_file_flags _flags
274
275 /* The following pointers correspond to the C++ streambuf protocol. */
276 /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
277 char* _IO_read_ptr; /* Current read pointer */
278 char* _IO_read_end; /* End of get area. */
279 char* _IO_read_base; /* Start of putback+get area. */
280 char* _IO_write_base; /* Start of put area. */
281 char* _IO_write_ptr; /* Current put pointer. */
282 char* _IO_write_end; /* End of put area. */
283 char* _IO_buf_base; /* Start of reserve area. */
284 char* _IO_buf_end; /* End of reserve area. */
285 /* The following fields are used to support backing up and undo. */
286 char *_IO_save_base; /* Pointer to start of non-current get area. */
287 char *_IO_backup_base; /* Pointer to first valid character of backup area */
288 char *_IO_save_end; /* Pointer to end of non-current get area. */
289
290 struct _IO_marker *_markers;
291
292 struct _IO_FILE *_chain;
293
294 int _fileno;
295 #if 0
296 int _blksize;
297 #else
298 int _flags2;
299 #endif
300 _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
301
302 #define __HAVE_COLUMN /* temporary */
303 /* 1+column number of pbase(); 0 is unknown. */
304 unsigned short _cur_column;
305 signed char _vtable_offset;
306 char _shortbuf[1];
307
308 /* char* _save_gptr; char* _save_egptr; */
309
310 _IO_lock_t *_lock;
311 #ifdef _IO_USE_OLD_IO_FILE
312 };
なんだか壮大なスケールの構造体の定義でしたが、目的のint_filenoを見つけ出すことができました。
ここで、あらかじめ定義されているファイルポインタstdin, stdout, stderrの定義内容を見てみます。
$ less -N /usr/include/stdio.h
164 /* Standard streams. */
165 extern struct _IO_FILE *stdin; /* Standard input stream. */
166 extern struct _IO_FILE *stdout; /* Standard output stream. */
167 extern struct _IO_FILE *stderr; /* Standard error output stream. */
なるほど。stdin, stdout, stderrは_IO_FILE型の構造体を参照するポインタとして定義されているのですね。
以上のことより、下のようなプログラムを作ることができます。
// test.c
#include<stdio.h> /* printf, stdin, stdout, stderr */
int main(void)
{
//ファイルポインタの宣言
FILE *fp;
fp = fopen("./inputfile", "r");
/*各々のファイルポインタが参照するFILE型の構造体の_filenoを
表示させる。
_filenoとはファイルシステムのiノードリスト内に存在する
iノードリトにある、「利用者ファイル記述表」というFILE型の構造体
が連結されたリストの順番を示す番号である。 */
/*stdin, stout, stderrはstdio.hで定義されている
ファイルポインタである。*/
printf("stdin : %d\n", stdin->_fileno);
printf("stdout : %d\n", stdout->_fileno);
printf("stderr : %d\n", stderr->_fileno);
printf("fp : %d\n", fp->_fileno);
//ファイルのクローズ
close(fp);
return 0;
}
#コンパイル
$ gcc -o test test.c
#実行
$ ./test
stdin : 0
stout : 1
stderr: 2
Segmentation fault
#inputfileという空ファイルを作成
$ touch inputfile
#3番のファイル記述子がちゃんと割り当てられていることが確認できる。
$ ./test
stdin : 0
stout : 1
stderr: 2
fp : 3
libc.aの中身を暴く
libc.aと言えば、普段UNIX環境でプログラミングを行うときにルーチンでリンクさせているライブラリのことです。
libc.aは実は独立した機能を持ったオブジェクトファイル(システムコールや、システムコールを呼び出すことによって実現される標準入出力関数など)がアーカイブされたものなのです。
今回は、libc.aを適当なディレクトリにコピーしてきて、そこで解凍してみて中身を検証してみました。
#libc.aのコピー
#libc.aを壊すと面倒なのでコピーしてしまうのである。
$ cp /usr/lib/libc.a .
#解凍
$ ar -x libc.a
libc.aは実は独立した機能を持ったオブジェクトファイル(システムコールや、システムコールを呼び出すことによって実現される標準入出力関数など)がアーカイブされたものなのです。
今回は、libc.aを適当なディレクトリにコピーしてきて、そこで解凍してみて中身を検証してみました。
#libc.aのコピー
#libc.aを壊すと面倒なのでコピーしてしまうのである。
$ cp /usr/lib/libc.a .
#解凍
$ ar -x libc.a
#生成したオブジェクトファイルは1510個もある!
$ ls | wc
1510 1510 17779
#代表的なシステムコールは含まれているか!?
$ ls -l open.o creat.o close.o read.o write.o lseek.o unlink.o
-rw-r--r-- 1 kappa kappa 1136 2012-07-15 21:46 close.o
-rw-r--r-- 1 kappa kappa 1120 2012-07-15 21:46 creat.o
-rw-r--r-- 1 kappa kappa 932 2012-07-15 21:46 lseek.o
-rw-r--r-- 1 kappa kappa 1156 2012-07-15 21:46 open.o
-rw-r--r-- 1 kappa kappa 1156 2012-07-15 21:46 read.o
-rw-r--r-- 1 kappa kappa 892 2012-07-15 21:46 unlink.o
-rw-r--r-- 1 kappa kappa 1160 2012-07-15 21:46 write.o
#代表的な標準入出力関数はあるか!?
$ ls -l scanf.o fprintf.o
-rw-r--r-- 1 kappa kappa 996 2012-07-15 21:46 fprintf.o
-rw-r--r-- 1 kappa kappa 1008 2012-07-15 21:46 scanf.o
kappa@kappa:~/test/artest$
libc.aは頻繁に使用される関数のオブジェクトファイルを内包しているという点でとても大切であることがよくわかりました。
SYSCALLS(2)
Linuxの全システムコールのリストを得たいときは、SYSCALLS(2)という第2章の目次のような項目が参考になります。
$ man syscalls
NAME
syscalls - Linux system calls
SYNOPSIS
Linux system calls.
DESCRIPTION
The system call is the fundamental interface between an application and the Linux kernel.
System call Kernel Notes
────────────────────────────────────────
_llseek(2) 1.2
_newselect(2)
_sysctl(2)
accept(2)
accept4(2) 2.6.28
access(2)
acct(2)
add_key(2) 2.6.11
adjtimex(2)
afs_syscall(2) Not implemented
alarm(2)
alloc_hugepages(2) 2.5.36 Removed in 2.5.44
bdflush(2)
bind(2)
break(2) Not implemented
brk(2)
cacheflush(2) 1.2 Not on i386
以下続く。
システムコールの総数が定義されているのは、/usr/include/asm/unistd_32.h
/*
* This file contains the system call numbers.
*/
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
以下続く。
ちなみに僕の環境(Ubuntu10.04、32bit)では、システムコールの総数は337個でした。
main()の第三引数の検証
main()には環境変数を格納する第三の引数(*envp[])が存在します。
*envp[]は環境変数を格納する文字列に対してのポインタ配列です。
以下のプログラムでは、envp[]が参照する配列の中身を順次表示させるものです。
/* myenv.c */
/* 第3の引数を検証するプログラム*/
#include<stdio.h> /* printf */
/*
main(int argc, char *argv[], char *envp[])
int argc;
char *argv[];
char *envp[];
*/
int main(int argc, char *argv[],char *envp[])
{
int i;
/* env[i]が存在する限り繰り返す */
for(i=0; envp[i]; i++){
printf("%s\n", envp[i]);
}
return 0;
}
*envp[]は環境変数を格納する文字列に対してのポインタ配列です。
以下のプログラムでは、envp[]が参照する配列の中身を順次表示させるものです。
/* myenv.c */
/* 第3の引数を検証するプログラム*/
#include<stdio.h> /* printf */
/*
main(int argc, char *argv[], char *envp[])
int argc;
char *argv[];
char *envp[];
*/
int main(int argc, char *argv[],char *envp[])
{
int i;
/* env[i]が存在する限り繰り返す */
for(i=0; envp[i]; i++){
printf("%s\n", envp[i]);
}
return 0;
}
gccが行っている作業を検証してみる
GCCはいくつかのコマンドによって構成されているらしいのである。
普段のgcc -o outfile infileと入力するだけだが、裏では様々なイベントが発生している。
gccは、
プリプロセッサ: /lib/cpp
コンパイラ : /usr/lib/gcc/i686-linux-gnu/4.4/cc1
アセンブラ : /usr/bin/as
リンカ: /usr/bin/lib
の4つのコマンド適宜呼び出すための、ドライバとしての働きをします。
以下に、ここのコマンドを個別に呼び出して、実行形式ファイルを作成する方法を示します。
#1.ソースコードの作成
$ vim hello.c
#include<stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
#2.プリプロセス(#で始まる内容の展開;ヘッダーファイルの取り込み、マクロの展開)
#プリプロセスの結果は標準出力に出されるので、リダイレクトしてファイルに出力する。
/lib/cpp hello.c > hello.i
#3.プリプロセスにより生成した中間ファイルをアセンブリソース(hello.s)にコンパイル
$ /usr/lib/gcc/i686-linux-gnu/4.4/cc1 hello.i
#4.アセンブラによりアセンブルを行いオブジェクトファイル(hello.o)を生成
$ /usr/bin/as hello.s -o hello.o
ld -o hello -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o /usr/lib/crti.o hello.o -lc /usr/lib/crtn.o
普段のgcc -o outfile infileと入力するだけだが、裏では様々なイベントが発生している。
gccは、
プリプロセッサ: /lib/cpp
コンパイラ : /usr/lib/gcc/i686-linux-gnu/4.4/cc1
アセンブラ : /usr/bin/as
リンカ: /usr/bin/lib
の4つのコマンド適宜呼び出すための、ドライバとしての働きをします。
以下に、ここのコマンドを個別に呼び出して、実行形式ファイルを作成する方法を示します。
#1.ソースコードの作成
$ vim hello.c
#include<stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
#2.プリプロセス(#で始まる内容の展開;ヘッダーファイルの取り込み、マクロの展開)
#プリプロセスの結果は標準出力に出されるので、リダイレクトしてファイルに出力する。
/lib/cpp hello.c > hello.i
#3.プリプロセスにより生成した中間ファイルをアセンブリソース(hello.s)にコンパイル
$ /usr/lib/gcc/i686-linux-gnu/4.4/cc1 hello.i
#4.アセンブラによりアセンブルを行いオブジェクトファイル(hello.o)を生成
$ /usr/bin/as hello.s -o hello.o
#5.オブジェクトファイル、共有ライブラリファイル(libc.so)、スタートアップルーチン(/usr/lib/crt1.o)等をダイナミックリンクさせ、実行形式ファイル(hello)を生成
$ /usr/bin/ld -o hello -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/libc.so /usr/lib/crtn.o hello.o
#6.実行形式ファイルを実行
$ ./hello
Hello, world!
理解すると非常にすっきりできるお話です。コンパイルは、入門書には非常に観念的なレベルのみ説明されていることがほとんだと思います。
今回のように、一つ一つんファイルをフルパスで指定していくと、コンパイルの本質が見てきます。
ちなみに、同様の作業は、gccに特定のオプションをつければ可能です。
ld -o hello -dynamic-linker /lib/ld-linux.so.2 /usr/lib/crt1.o /usr/lib/crti.o hello.o -lc /usr/lib/crtn.o
MacでOpenGL
Mac OS Xの場合(10.6)、デフォルトでOpenGLがインストールされています。
$ gcc -framework GLUT -o file sourcefile.c
のように入力すればコンパイル可能です。
$ gcc -framework GLUT -o file sourcefile.c
のように入力すればコンパイル可能です。
ubuntuにOpenGLをインストール
$ sudo apt-get update
$ sudo apt-get install freeglut3-dev libglew1.5-dev
#テスト
$ vim test.c
/*test.c*/
#include <GL/glut.h>
int main(int argc,char *argv[])
{
/*初期化*/
glutInit(&argc,argv);
/*ウィンドウ作成*/
glutCreateWindow(argv[0]);
/*メインループ*/
glutMainLoop();
return 0;
}
$ gcc -lglut -o test test.c
$ sudo apt-get install freeglut3-dev libglew1.5-dev
#テスト
$ vim test.c
/*test.c*/
#include <GL/glut.h>
int main(int argc,char *argv[])
{
/*初期化*/
glutInit(&argc,argv);
/*ウィンドウ作成*/
glutCreateWindow(argv[0]);
/*メインループ*/
glutMainLoop();
return 0;
}
#コンパイル
$ gcc -lglut -o test test.c
システムコールrename(2)を用いたmvコマンドの作成
unixコマンドで比較的頻繁に使用するmvコマンド。
ファイルの名前を変更したり、ディレクトリを移動させたりするのに使用します。
mvを実装するには、ファイルを移動するためのAPIであるrename(2)を使用します。
//mymv.c
#include<stdio.h> /* fprintf, perror */
#include<stdlib.h> /* exit */
#include<unistd.h>
int main(int argc, char * argv[])
{
//エラー処理
if(argc !=3){
fprintf(stderr, "%s: wrong arguments\n", argv[0]);
exit(1);
}
if(rename(argv[1], argv[2])<0){
perror(argv[1]);
exit(1);
}
exit(0);
}
ウィンドウにtuxを表示させる
GTK+を用いたプログラミングの基本的な手法を組み合わせて
ウィンドウにtuxを表示させるプログラムを作って見ました。
ウェブアプリは簡単にさまざまなことができて確かに楽しいですが、
やはりローカル環境でスタンドアロンで動くGTK+のCプログラミングも奥が深くて面白いですね。
//showtux.c
#include<gtk/gtk.h>
// gtk_main_quit()を呼び出すコールバック関数を作成
// typedef void* gpointer
static void button_clicked(GtkWidget *button, gpointer user_data)
{
gtk_main_quit();
}
// main関数
int main(int argc, char **argv)
{
//初期化
gtk_init(&argc, &argv);
//一番の土台となるwindowウェジェットの生成及び各種設定
//windowオブジェクトの生成
GtkWidget *window;
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_widget_set_size_request(window, 300, 400);
//ツールチップヒントの作成。
// typedef gchar char gchar
gchar *text;
// gchar* gstrdup(const gchar *str);
text = g_strdup("We love linux programming!!");
gtk_widget_set_tooltip_text(window, text);
//アイコンを設定する。
gtk_window_set_default_icon_from_file("./tux.png", NULL);
//以下のスコープで画像とボタンをのせるためのバッキンボックス(vbox)を作成し、
//windowウェジェットに貼り付ける
{
GtkWidget *vbox;
vbox = gtk_vbox_new(FALSE, 2);
gtk_container_add(GTK_CONTAINER(window), vbox);
//以下のスコープで画像とボタンのウェジェットを作成し、
//パッキンボックス(vbox)上に貼り付ける。
{
GtkWidget *image;
GtkWidget *button;
image = gtk_image_new_from_file("./tux.png");
//パッキンボックス(vbox)上に貼り付ける。
gtk_box_pack_start(GTK_BOX(vbox), image, TRUE, TRUE, 0);
button = gtk_button_new_with_label("Quit");
//パッキンボックス(vbox)上に貼り付ける。
gtk_box_pack_start(GTK_BOX(vbox), button , FALSE, FALSE, 0);
//ボタンに関しては、clicked動作に際して、冒頭に作成したbutton_clicked関数を
//コールバックするように設定。
//g_sginal_connect connects a GCallback function
// to a signal for a particular object
g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(button_clicked), NULL);
}
}
//windowを閉じられた時は、gtk_main_quitが呼び出されるようにする。
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
gtk_window_set_default_icon_from_file("tux.png", NULL);
//生成したすべてのウェジェットを可視化する。
gtk_widget_show_all(window);
//メインループの開始
gtk_main();
return 0;
}
forkを用いた簡単なプログラム
プロセスを複製するためのシステムコールであるforkを用いた簡単なプログムを作りました。
大事な構文は、
if(fork() == 0){
/*child process */
} else {
/* parent process */
}
というように、if文を用いて親と子の各々の処理を記述することがあげられます。
// fork_test.c
#include<stdio.h> /* printf */
#include<stdlib.h> /* exit */
#include<sys/types.h> /* fork */
int main(void)
{
pid_t pid;
pid = fork();
//エラー処理
if(pid < 0 ){
fprintf(stderr, "fork(2) failed\n");
exit(1);
}
if(pid==0){
//子プロセスのためのコード
printf("Hello, world from child process.\n");
exit(0);
}
else{
//親プロセスのためのコード
printf("Hello, world from parent process.\n");
exit(0);
}
}
GTKでHello, world(改)
以前書いたgtkを使用したHello, worldプログラムをもう少し機能追加を行いました。
追加項目「ツールヒントの記述」「アイコン表示」の2点です。
//hello_gtk.c
#include<gtk/gtk.h>
int main(int argc, char **argv){
GtkWidget *window;
//ウィンドウウェジェットを作成。
gtk_init(&argc, &argv);
//ウィンドウウェジェットを作成。
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
//ウィンドウのサイズを規程。
gtk_widget_set_size_request(window, 500, 600);
//ウィンドウのタイトルを規定する。
gtk_window_set_title(GTK_WINDOW(window), "Hello, world in GTK!!");
//カレントディレクトリに置いてあるファイルを読み込んでアイコンにする。
gtk_window_set_default_icon_from_file("./tux.png", NULL);
//ウィンドウを閉じたときの処理を規定する。
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
//ウィンドウを表示
gtk_widget_show(window);
//ツールチップヒントの作成。
gchar *text = g_strdup("Hello, world in GTK");
gtk_widget_set_tooltip_text(window, text);
g_free(text);
//メインループ開始。
gtk_main();
return 0;
}
追加項目「ツールヒントの記述」「アイコン表示」の2点です。
//hello_gtk.c
#include<gtk/gtk.h>
int main(int argc, char **argv){
GtkWidget *window;
//ウィンドウウェジェットを作成。
gtk_init(&argc, &argv);
//ウィンドウウェジェットを作成。
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
//ウィンドウのサイズを規程。
gtk_widget_set_size_request(window, 500, 600);
//ウィンドウのタイトルを規定する。
gtk_window_set_title(GTK_WINDOW(window), "Hello, world in GTK!!");
//カレントディレクトリに置いてあるファイルを読み込んでアイコンにする。
gtk_window_set_default_icon_from_file("./tux.png", NULL);
//ウィンドウを閉じたときの処理を規定する。
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
//ウィンドウを表示
gtk_widget_show(window);
//ツールチップヒントの作成。
gchar *text = g_strdup("Hello, world in GTK");
gtk_widget_set_tooltip_text(window, text);
g_free(text);
//メインループ開始。
gtk_main();
return 0;
}
コマンドライン引数が指定する画像をウィンドウに表示させるプログラムです。
画像ウェジェットを作成してバッキンボックス上に張り付けてあげればOKです。
/* gtk_picutre.c */
#include<gtk/gtk.h>
#include<stdlib.h> //exit
int main(int argc, char *argv[]){
//引数のエラー処理
if(argc!=2){
g_print("Usage: %s image-file\n", argv[0]);
exit(1);
}
GtkWidget *window;
//gtk+を初期化
gtk_init(&argc, &argv);
//ウィンドウェジェットを作成
window=gtk_window_new(GTK_WINDOW_TOPLEVEL);
//ウィンドウの大きさを指定
gtk_widget_set_size_request(window, 600, 400);
//パッキンボックスを作成
GtkWidget *vbox;
vbox = gtk_vbox_new(FALSE,2);
//パッキンボックスをwindowに貼り付ける
gtk_container_add(GTK_CONTAINER(window), vbox);
//画像ウェジェットを作成
GtkWidget *image;
//コマンドライン引数が指定するファイルから画像ウェジェットを作成
image = gtk_image_new_from_file(argv[1]);
//画像ウェジェットをvboxの上半分に貼り付ける
gtk_box_pack_start(GTK_BOX(vbox), image, TRUE, TRUE, 0);
//ボタンを作成
GtkWidget *button;
button = gtk_button_new_with_label("quit");
//ボタンをクリックした時のコールバック関数を登録
g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(gtk_main_quit),NULL);
//buttonをvboxの下半分に貼り付ける
gtk_box_pack_start(GTK_BOX(vbox), button, FALSE,FALSE, 0);
//ウィンドを閉じたときにプログラムを終了するようにコールバックする。
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
//ウィンドウ上のウェジェットをすべて表示(gtk_widget_show_all)
gtk_widget_show_all(window);
gtk_main();
return 0;
}
#使用例
#引数を与えないとエラーがでます
$ ./gtk_picture
Usage: ./gtk_picture image-file
#current directoryにtux.pngという画像ファイルを置いた状態で以下のように引数をつけてプログラムを実行
$ ./gtk_picture tux.png