青木 峰郎 SBクリエイティブ 2017-09-22
こんにちは。
最近、ふつうのLinuxプログラミング 第2版を読んでいるので知識の定着のために学んだ内容を要約したメモを書きます。
このエントリは完全な個人のメモです。
お勉強のためにこの本を読んでいるので、内容を覚えるためにSummarizing(サマライジング)を行います。
10章前半の内容はこちら。
amistad06-a.hatenablog.com
第10章 ファイルシステムにかかわるAPI(前回の続き)
ファイルを消す
Linuxにおいて、ファイルを消すということは実体につけた名前を減らす(リンクを削除する)ということです。
※正確に言うと名前がなくなる+参照がなくなるとファイルが消える
unlink(2)
リンクを削除するシステムコール。
成功したら0、失敗したら-1を返します。
このAPIではディレクトリは消せません。
また、シンボリックリンクをunlinkするとシンボリックリンクだけが削除されて実体は削除されません。
rmコマンドを作る
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]){
int i;
// コマンドライン引数で削除対象が与えられていない場合、
// プログラムを終了する
if(argc < 2){
fprintf(stderr, "%s: no arguments\n", argv[0]);
exit(1);
}
// 複数のファイルの指定が可能なようにfor文で回す
for(i = 1; i < argc; i++){
// リンクを削除する
// 削除に失敗した場合、プログラムを終了する
if(unlink(argv[i]) < 0){
perror(argv[i]);
exit(1);
}
}
exit(0);
}
プログラムの説明はプログラム中のコメントを参照。
ファイルを移動する
Linuxにおいて、ファイルを移動するというのは実体に対応する名前(リンク)を付け替えることと同義です。
rename(2)
ファイル名を変更(移動)するシステムコール。
成功したら0、失敗したら-1を返します。
ファイルシステムをまたぐ移動はできません。
mvコマンドを作る
#include <stdio.h>
#include <stdlib.h>
#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);
}
プログラムの説明はプログラム中のコメントを参照。
ファイルの付帯情報
Linuxのファイルには、データ本体の他に以下のような付帯情報があります。
・ファイルの種類
・サイズ
・パーミッション
・所有者
・グループ
・作成時刻
・変更時刻
・アクセス時刻
stat(2), lstat(2)
引数で与えられたエントリの情報を取得します。
成功したら0、失敗したら-1を返します。
lstatはシンボリックリンクを渡された場合、リンクをたどらずにシンボリックリンク自身の情報を返します。
ファイルのパスではなくファイルディスクリプタから情報を得るfstat(2)という関数もあります。
struct statにどのような情報が含まれるのかはmanを参照してください。
ラージファイル対応
struct statのメンバはすべてtype_defされています。
これは可搬性を高めるためでもあるのですが、ラージファイル対応のためでもあります。
たとえば、32bit型のOSではlong型は32bitなので2GBまでしか対応できません。
これでは大きなファイルのサイズを適切に処理できません。
そこで、#define _FILE_OFFSET_BITS64と定義してビルドをするとoff_t型が64bitのlong long型になるのでより大きな範囲を表現できるようになります。
このような仕組みをラージファイル対応といいます。
statコマンドを作る
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
static char *filetype(mode_t mode);
int main(int argc, char *argv[]){
struct stat st;
// 引数でファイル名を指定されていない場合、プログラムを終了する
if(argc != 2){
fprintf(stderr, "wrong argument\n");
exit(1);
}
// 付帯情報を取得する
if(lstat(argv[1], &st) < 0){
perror(argv[1]);
exit(1);
}
// 取得した付帯情報を表示する
printf("type\t%o (%s)\n", (st.st_mode & S_IFMT), filetype(st.st_mode));
printf("mode\t%o\n", st.st_mode & ~S_IFMT);
printf("dev\t%llu\n", (unsigned long long)st.st_dev);
printf("ino\t%lu\n", (unsigned long)st.st_ino);
printf("rdev\t%llu\n", (unsigned long long)st.st_rdev);
printf("nlink\t%lu\n", (unsigned long)st.st_nlink);
printf("uid\t%d\n", st.st_uid);
printf("gid\t%d\n", st.st_uid);
printf("size\t%ld\n", st.st_size);
printf("blksize\t%lu\n", (unsigned long)st.st_blksize);
printf("blocks\t%lu\n", (unsigned long)st.st_blocks);
printf("atime\t%s", ctime(&st.st_atime));
printf("mtime\t%s", ctime(&st.st_mtime));
printf("ctime\t%s", ctime(&st.st_ctime));
exit(0);
}
static char *filetype(mode_t mode){
// ファイルタイプを判定する
// それぞれのタイプを判定するマクロがあるのでそれを使う
if(S_ISREG(mode)){
return "file";
}
if(S_ISDIR(mode)){
return "directory";
}
if(S_ISCHR(mode)){
return "chardev";
}
if(S_ISBLK(mode)){
return "blockdev";
}
if(S_ISFIFO(mode)){
return "fifo";
}
if(S_ISSOCK(mode)){
return "socket";
}
return "unknown";
}
プログラムの説明はプログラム中のコメントを参照。
付帯情報を変更する
付帯情報を変更するシステムコールは以下の3つ。
chmod
chown
utime
chmod(2)
パーミッションを変更するシステムコール。
成功したら0、失敗したら-1を返します。
パーミッションの指定の仕方は色々あるので詳細はman 参照。
chown(2), lchown(2)
所有ユーザと所有グループを変更するシステムコール。
lchownはシンボリックリンクを指定されたときに、そのシンボリックリンク自体の情報を変更します。
成功したら0、失敗したら-1を返します。
utime(2)
最終アクセス時刻と最終更新時刻を変更します。
成功したら0、失敗したら-1を返します。
chmodコマンドを作る
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(int argc, char *argv[]){
int mode;
int i;
// 変更したいパーミッションが与えられていない場合、
// プログラムを終了する
if(argc < 2){
fprintf(stderr, "no mode given\n");
exit(1);
}
// 引数で与えられたパーミッションを
// 文字列からlong int型(8進数)に変換する
mode = strtol(argv[1], NULL, 8);
// 引数で与えられたファイルを全て操作するためにfor文で回す
for(i = 2; i < argc; i++){
// パーミッションを変更
if(chmod(argv[i], mode) < 0){
// 失敗したらエラー分を表示する
perror(argv[i]);
}
}
exit(0);
}
プログラムの説明はプログラム中のコメントを参照。
ファイルシステムとストリーム
open()はO_CREATフラグを使用するとファイルの作成ができます。
そのため、open()はファイルシステムのAPIでもあります。
open()もumaskの制限を受けます。
演習問題
1. コマンドライン引数で指定されたディレクトリを再帰的にトラバースして、見つかったファイルのパスをすべて表示するプログラムを書きなさい。シンボリックリンクをたどってはいけません。
サポートサイトを見る前に書いたソースコードを載せます。
サポートサイトの回答例とは全然違った作りになりました。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>
static void do_lsr(char *path);
static char *check_last_char(char *path);
int main(int argc, char *argv[]){
// コマンドライン引数で表示するディレクトリのパスをもらう
// コマンドライン引数がない場合、プログラムを終了
if(argc < 2){
fprintf(stderr, "%s: no arguments\n", argv[0]);
exit(1);
}
for(int i = 1; i < argc; i++){
char *path;
// sprintf(path, "%s", argv[i]);
path = check_last_char(argv[i]);
do_lsr(path);
}
}
static void do_lsr(char *path){
DIR *d;
struct dirent *ent;
// ディレクトリ名を表示
printf("%s\n", path);
// ディレクトリをopen
d = opendir(path);
if(!d){
printf("opendir error.\n");
perror(path);
exit(1);
}
// エントリがなくなるまで繰り返し
// =演算子は代入された値を返す
while(ent = readdir(d)){
// エントリの種別が取得できない場合、次のエントリを表示
if(ent->d_type == DT_UNKNOWN){
//printf("d_type is unknonwn.\n");
continue;
}
// エントリがシンボリックリンクの場合、次のエントリを表示
// 無限ループを避けるため
if(ent->d_type == DT_LNK){
//printf("symboric link.\n");
continue;
}
// エントリがディレクトリだったら、その中のファイルも表示する
if(ent->d_type == DT_DIR){
// カレントディレクトリと親ディレクトリの場合、次のエントリを表示
// 無限ループを避けるため
// strcmpは文字列を比較し、等しかったら0を返す
if(!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")){
continue;
}
char child_dir[1024];
sprintf(child_dir, "%s%s/", path, ent->d_name);
// 再帰呼び出し
do_lsr(child_dir);
}else{
// ディレクトリ以外のものはファイル名を表示
printf("%s%s\n", path, ent->d_name);
}
}
// ファイル操作と同様、openしたストリームは必ずcloseする
closedir(d);
}
static char *check_last_char(char *path){
// pathの一番最後の文字を見て、
// "/"じゃなかったら"/"を追加
int len = strlen(path);
char tmp[1024];
if(path[len - 1] != '/'){
sprintf(tmp, "%s/", path);
path = tmp;
}
return path;
}
2. ファイルをopen()して、close()する前にそのファイルをrename()すると何が起きるでしょうか。ulink()はどうか、別のファイルをrenameするとどうなるか、実験して調べなさい。
検証したプログラムは以下。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]){
// コマンドライン引数でオープンするファイル名をもらう
// コマンドライン引数がない場合、プログラムを終了
if(argc != 2){
fprintf(stderr, "%s: no arguments\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDONLY);
//int fd = open(argv[1], O_WRONLY);
//int fd = open(argv[1], O_RDWR);
if(fd < 0){
printf("error open failed.");
return 1;
}
int ret = rename(argv[1], "renamed.txt");
if(ret < 0){
printf("error rename failed.");
return 1;
}
char buf[1024];
int n;
for(;;){
n = read(fd, buf, sizeof buf);
if(n < 0){
printf("error read failed");
return 1;
}
if(n == 0){
break;
}
if(write(STDOUT_FILENO, buf, n) < 0){
printf("error write failed");
return 1;
}
}
ret = close(fd);
if(ret < 0){
printf("error close failed");
return 1;
}
return 0;
}
openしてからcloseする前にrenameした場合、openしたFDを使ってそのままそのファイルの中身を見ることができる。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]){
// コマンドライン引数でオープンするファイル名をもらう
// コマンドライン引数がない場合、プログラムを終了
if(argc != 2){
fprintf(stderr, "%s: no arguments\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDONLY);
//int fd = open(argv[1], O_WRONLY);
//int fd = open(argv[1], O_RDWR);
if(fd < 0){
printf("error open failed.");
return 1;
}
int ret = unlink(argv[1]);
if(ret < 0){
printf("error unlink failed.");
return 1;
}
ret = rename("tmp1.txt", argv[1]);
char buf[1024];
int n;
for(;;){
n = read(fd, buf, sizeof buf);
if(n < 0){
printf("error read failed");
return 1;
}
if(n == 0){
break;
}
if(write(STDOUT_FILENO, buf, n) < 0){
printf("error write failed");
return 1;
}
}
ret = close(fd);
if(ret < 0){
printf("error close failed");
return 1;
}
return 0;
}
unlink()した場合もFDをクローズするまでファイルの中身を見ることができる。unlinkした場合、プログラムが終了したらファイルは消える。
unlinkしたあと別のファイルをopenしたファイル名に変更した場合、もとのファイルの中身を見ることができる。
3. コマンドライン引数で指定されたパスまでのディレクトリを再帰的に作成するコマンドを書きなさい(mkdir -pに相当)。
サポートサイトを見る前に書いたソースコードを載せます。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
int mkdirp(char *path);
int main(int argc, char *argv[]){
int i;
// コマンドライン引数で作りたいディレクトリのパスを受け取る
// コマンドライン引数がない場合、プログラムを終了
if(argc < 2){
fprintf(stderr, "%s: no arguments\n", argv[0]);
exit(1);
}
for(i = 1; i < argc; i++){
mkdirp(argv[i]);
}
exit(0);
}
int mkdirp(char *path){
// mkdir(2)でディレクトリを作成
// 成功したら0、失敗したら-1が変える
int ret = mkdir(path, 0777);
if(ret == 0){
return 0;
}
if(errno == EEXIST){
printf("the directory is already exists.");
return EEXIST;
}else if(errno == ENOENT){
// pathを複製
char * parent = strdup(path);
int len = strlen(parent);
char last = parent[len - 1];
if(last == '/'){
parent[len - 1] = '\0';
}
// 最後の"/"までのpathを取得
char *ptr = strrchr(parent, '/');
*ptr = '\0';
// 再帰呼び出し
mkdirp(parent);
// 親ディレクトリができているはずなので再度mkdir
ret = mkdir(path, 0777);
if(ret < 0){
printf("error mkdir failed");
return ret;
}
}else{
printf("other error.\n");
printf("%d\n", ret);
return ret;
}
}
※strdupではmallocで文字列のメモリを確保しているため、複製した文字列を使い終わったらfreeしなければならない。上記のプログラムではしていない。
まとめ
10章はこんな感じです。
第11章はこちら。
amistad06-a.hatenablog.com
おわり。
青木 峰郎 SBクリエイティブ 2017-09-22