home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/「デーモン君のソース探検」読書メモ/A01, touch(1) (v1)

C言語系/「デーモン君のソース探検」読書メモ/A01, touch(1) (v1)

C言語系 / 「デーモン君のソース探検」読書メモ / A01, touch(1) (v1)
id: 573 所有者: msakamoto-sf    作成日: 2010-02-03 14:41:07
カテゴリ: BSD C言語 

お題:touch(1)がファイルの日付属性を操作する仕組みを調査せよ

※この章は「デーモン君のソース探検」に載っていませんが、msakamoto-sf自身が個人的に興味を持って調べ、"Appendix"として読書メモシリーズに入れてありますのでご注意下さい。

$ which touch
/usr/bin/touch
$ locate touch.c
...
/usr/src/usr.bin/touch/touch.c

使う時のオプションを確認しておく。

SYNOPSIS
     touch [-acfhm] [-r file] [-t [[CC]YY]MMDDhhmm[.SS]] file ...

オプションの簡単なまとめ:

-a : アクセス時間を更新。"-m"が指定されない限り、変更時間は更新されない。
-c : ファイルが無い場合はファイルを作成せずそのまま終了。
-f : 権限を無視して強制更新。
-h : シンボリックリンクの場合は、リンクそれ自体の時間を変更。
    ("-h"無しのデフォルトの場合、シンボリックリンクの参照先ファイルの時間を変更する)
-m : 変更時間を更新。"-a"が指定されない限り、アクセス時間は更新されない。
-r : 現在日時の代わりに、指定されたファイルのアクセス/変更時間に更新。
-t : 時間を指定

また、昔 "-r" or "-t" を使わない次のような時間指定方法が在ったため、後方互換性維持の為サポートしている。

touch [-acfhm] MMDDhhmm[yy] file ...

では、早速touch.cを読み進めていく。特にグローバル変数の宣言は無いので、main()関数から始める。

main()関数

変数宣言などの冒頭部分については特に難しいところはなく、下記コードブロックのコメント解説を参照。

int
main(argc, argv)
  int argc;
  char *argv[];
{
  struct stat sb; /* stat(2) or lstat(2) 用 */
  struct timeval tv[2]; /* 現在時間がtv[0], 更新する時間がtv[1] */
  int aflag, cflag, fflag, hflag, mflag, ch, fd, len, rval, timeset;
  char *p;
  /* "-h"オプション(シンボリックリンク)に応じて ... */
  /* lutimes(2)/utimes(2)を切り替える */
  int (*change_file_times) __P((const char *, const struct timeval *));
  /* lstat(2)/stat(2)を切り替える */
  int (*get_file_status) __P((const char *, struct stat *));

  /* ロカール情報をデフォルト(C)にセット */
  setlocale(LC_ALL, "");

  aflag = cflag = fflag = hflag = mflag = timeset = 0;
  /* 現在時間(tv[0])をセット */
  if (gettimeofday(&tv[0], NULL))
    err(1, "gettimeofday");

続いてオプション解析に進む。getopt(3)を用いた標準的なコードになっている。

  while ((ch = getopt(argc, argv, "acfhmr:t:")) != -1)
    switch(ch) {
    case 'a':
      aflag = 1;
      break;
    case 'c':
      cflag = 1;
      break;
    case 'f':
      fflag = 1;
      break;
    case 'h':
      hflag = 1;
      break;
    case 'm':
      mflag = 1;
      break;
    case 'r':
      timeset = 1;
      stime_file(optarg, tv);
      break;
    case 't':
      timeset = 1;
      stime_arg1(optarg, tv);
      break;
    case '?':
    default:
      usage();
    }
  argc -= optind;
  argv += optind;

"-r"の場合はstime_file()関数、"-t"の場合はstime_arg1()関数を呼んでいる。上で簡単にまとめたオプションの使い方と引数にtvがあることから、変更する時間をファイル or コマンドライン引数から計算してtvに格納していることが予想される。
後ろにstime_arg2()関数の呼び出しもあるため、一通り出揃ったところで確認する。

struct stat, struct timespec

ここで一旦、stat構造体やその時刻フィールド周りをおさらいしておく。
stat構造体:stat(2)参照

struct stat {
    dev_t     st_dev;     /* device containing the file */
    ino_t     st_ino;     /* file's serial number */
    mode_t    st_mode;    /* file's mode (protection and type) */
    nlink_t   st_nlink;   /* number of hard links to the file */
    uid_t     st_uid;     /* user-id of owner */
    gid_t     st_gid;     /* group-id of owner */
    dev_t     st_rdev;    /* device type, for device special file */
    struct timespec st_atimespec;  /* time of last access */
    struct timespec st_mtimespec;  /* time of last data modification */
    struct timespec st_ctimespec;  /* time of last file status change */
    off_t     st_size;    /* file size, in bytes */
    int64_t   st_blocks;  /* blocks allocated for file */
    u_int32_t st_blksize; /* optimal file sys I/O ops blocksize */
    u_int32_t st_flags;   /* user defined flags for file */
    u_int32_t st_gen;     /* file generation number */
};

The time-related fields of struct stat are as follows:

st_atime     Time when file data was last accessed.  Changed by the
             mknod(2), utimes(2) and read(2) system calls.

st_mtime     Time when file data was last modified.  Changed by the
             mknod(2), utimes(2) and write(2) system calls.

st_ctime     Time when file status was last changed (file metadata modi-
             fication).  Changed by the chflags(2), chmod(2), chown(2),
             link(2), mknod(2), rename(2), unlink(2), utimes(2) and
             write(2) system calls.

"struct timespec"というのが出てきているが、こちらは "sys/time.h" で定義されている。
sys/time.h:

/*
 * Structure defined by POSIX.1b to be like a timeval.
 */
struct timespec {
        time_t  tv_sec;         /* seconds */
        long    tv_nsec;        /* and nanoseconds */
};

/* struct timeval → struct timespec へ変換 */
#define TIMEVAL_TO_TIMESPEC(tv, ts) {          \
        (ts)->tv_sec = (tv)->tv_sec;           \
        (ts)->tv_nsec = (tv)->tv_usec * 1000;  \
}
/* struct timespec → struct timeval へ変換 */
#define TIMESPEC_TO_TIMEVAL(tv, ts) {          \
        (tv)->tv_sec = (ts)->tv_sec;           \
        (tv)->tv_usec = (ts)->tv_nsec / 1000;  \
}

stat構造体のst_{a|m|c}timeにアクセスしようとすると、"sys/stat.h"中の次のdefineにより自動的に"st_{a|m|c}timespec"メンバの"tv_sec"へのアクセスに展開される。
sys/stat.h:

#if !defined(_POSIX_C_SOURCE) && !defined(_XOPEN_SOURCE)
#define st_atime        st_atimespec.tv_sec
#define st_atimensec    st_atimespec.tv_nsec
#define st_mtime        st_mtimespec.tv_sec
#define st_mtimensec    st_mtimespec.tv_nsec
#define st_ctime        st_ctimespec.tv_sec
#define st_ctimensec    st_ctimespec.tv_nsec
#endif

main()関数の続き

main()関数に戻ると、オプション解析後の調整処理が続く。"-a"/"-m"のデフォルト調整、"-h"に応じた"lstat(2),lutimes(2)"/"stat(2),utimes(2)"の切り替え、古い時刻指定の解析(stime_arg2())、などが行われている。

  /* Default is both -a and -m. */
  if (aflag == 0 && mflag == 0)
    aflag = mflag = 1;

  if (hflag) {
    cflag = 1;        /* Don't create new file */
    change_file_times = lutimes;
    get_file_status = lstat;
  } else {
    change_file_times = utimes;
    get_file_status = stat;
  }

  /*
   * If no -r or -t flag, at least two operands, the first of which
   * is an 8 or 10 digit number, use the obsolete time specification.
   */
  if (!timeset && argc > 1) {
    (void)strtol(argv[0], &p, 10);
    len = p - argv[0];
    if (*p == '\0' && (len == 8 || len == 10)) {
      timeset = 1;
      stime_arg2(*argv++, len == 10, tv);
    }
  }

  /* Otherwise use the current time of day. */
  if (!timeset)
    /* stime_{arg1|arg2|file}()のいずれも呼ばれていない場合 */
    tv[1] = tv[0];

  if (*argv == NULL)
    usage();

更新時間を文字列orファイルから取得するstime_{arg1|arg2|file}()が出揃ったのでざっくりと解説する。

  • stime_arg1 : "-t"で指定された文字列を解析し、tv[0], tv[1]にセットする。
  • stime_arg2 : 古い方式で指定された文字列を解析し、tv[0], tv[1]にセットする。
  • stime_file : "-r"で指定されたファイルのstat情報から、TIMESPEC_TO_TIMEVALでatimeをtv[0]に、mtimeをtv[1]にセットする。

本筋から離れる為、これらの関数の内部には踏み込まない。

main()関数に戻ると、続くforループが実際に時刻を更新する処理になる。前半部分が正常系のルートになっている。

  for (rval = 0; *argv; ++argv) {
    /* See if the file exists. (stat(2) or lstat(2))*/
    if ((*get_file_status)(*argv, &sb)) {
      /* 存在しない or エラーの場合 */
      if (!cflag) {
        /* Create the file. (ファイル作成を試みる) */
        fd = open(*argv,
            O_WRONLY | O_CREAT, DEFFILEMODE);
        if (fd == -1 || fstat(fd, &sb) || close(fd)) {
          rval = 1;
          warn("%s", *argv);
          continue;
        }

        /* If using the current time, we're done. */
        /* (ファイル作成成功で、"-r"/"-t"による時刻指定が無ければ
            このファイルについては処理終了ということで、continueする */
        if (!timeset)
          continue;
      } else
        continue;
    }
    if (!aflag)
      TIMESPEC_TO_TIMEVAL(&tv[0], &sb.st_atimespec);
    if (!mflag)
      TIMESPEC_TO_TIMEVAL(&tv[1], &sb.st_mtimespec);

    /* Try utimes(2). (utimes(2) or lutimes(2)) */
    if (!(*change_file_times)(*argv, tv))
      continue;

次の4行が少し混乱する。

if (!aflag)
  TIMESPEC_TO_TIMEVAL(&tv[0], &sb.st_atimespec);
if (!mflag)
  TIMESPEC_TO_TIMEVAL(&tv[1], &sb.st_mtimespec);

まずsbの方は変更前のファイルのstat情報。tvの方は変更予定の時刻。ここで、stime_{arg1|arg2|file}()が実行された時点ではtv[0]/tv[1]ともに変更予定の時刻になっている点を思い出せば混乱も収まってくる。
aflagが指定されていない場合は、atimeは変更「しない」のだから、元のstat情報(=sb)のst_atimespecをtv[0]にコピーする。
mflagが指定されていない場合は、mtimeは変更「しない」のだから、元のstat情報(=sb)のst_mtimespecをtv[1]にコピーする。

最終的に(l)utimes(2)で指定するのはtv[0]/tv[1]になるので、"-a"/"-m"の挙動はこれでマニュアル通りになる。

forループの後半はutimes(2)/lutimes(2)のエラー時の処理になる。

    /* If the user specified a time, nothing else we can do. */
    if (timeset) {
      rval = 1;
      warn("%s", *argv);
    }

    /*
     * System V and POSIX 1003.1 require that a NULL argument
     * set the access/modification times to the current time.
     * The permission checks are different, too, in that the
     * ability to write the file is sufficient.  Take a shot.
     */
     if (!(*change_file_times)(*argv, NULL))
      continue;

    /* Try reading/writing. */
    if (!S_ISLNK(sb.st_mode) && rw(*argv, &sb, fflag))
      rval = 1;
  }
  exit(rval);

まず"-r" or "-t"で時刻指定された場合は、これ以上やりようが無い為 "rval"に1をセットし、warn()する。そのままcontinueはせず、現在時刻でも良いのでutimes(2)/lutimes(2)できないか、ファイルの読み書きで更新出来ないか試みようとする。
utimes(2)/lutimes(2)では、tvにNULLが指定された場合は現在時刻でatime/mtimeを更新する。これで成功した場合は、continueして次のファイルへ進む。
もしそれでも駄目な場合は、先頭1バイトを読み書きすることで更新を試みる。それがrw()関数の中身となる。
rw()の詳細は載せないが、"-f"が指定された場合はchmod()で一時的にpermissionを変更する。関数を抜ける時に戻している。メインのコードでは、先頭1バイトの読み書き or ファイルサイズ0なら1バイト書いてftruncate(2)することでファイル時刻の更新を試みている。

以上で、touch(1)がファイル時刻を更新する仕組みは utimes(2)/lutimes(2) を使うことで実現していることが判明した。

utime()とutimes()

実はutimes()の他にutime()が存在している。

utimes(const char *, const struct timeval *) : sys/time.h
utime(const char *, const struct utimbuf *)  : utime.h

utime()の方で時刻指定するutimbufは以下の構造体になっており、1秒単位での指定になる。
utime.h:

struct utimbuf {
  time_t actime;          /* Access time */
  time_t modtime;         /* Modification time */
};

一方、utimes()の方での時刻指定はtimeval構造体になっており、マイクロ秒単位まで指定出来る。
sys/time.h:

/*
 * Structure returned by gettimeofday(2) system call,
 * and used in other calls.
 */
struct timeval {
  long    tv_sec;         /* seconds */
  long    tv_usec;        /* and microseconds */
};

NetBSD1.6ではutime()はセクション3に分類され、manページにあるように utimes(2) の登場により廃止される運命にあるらしい。

UTIME(3)                  NetBSD Programmer's Manual                  UTIME(3)

NAME
     utime - set file times

LIBRARY
     Standard C Library (libc, -lc)

SYNOPSIS
     #include <utime.h>

     int
     utime(const char *file, const struct utimbuf *timep);

DESCRIPTION
     This interface is obsoleted by utimes(2).

またLinuxのjmanページにおいては「utime()システムコール」と表記されているものの、同様に将来的には廃止予定であると書かれている。
APUE第2版ではutime()のままで、utimes()の記載は無いので注意が必要である。

今回のお題については、ここまで。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-02-03 14:47:04
md5:ed006dde6f6b79e82895cc6ca9caa512
sha1:1c27282d3c027856f401be72027062ada7fdb665
コメント
コメントを投稿するにはログインして下さい。