【GNU coreutils】uname ソースコードリーディング

uname(1)は, 現在稼働中のカーネルについての名前と情報を得ることができるコマンドです。

$ uname -a
Darwin MacBook-Air.local 13.0.0 Darwin Kernel Version 13.0.0: Thu Sep 19 22:22:27 PDT 2013; root:xnu-2422.1.72~6/RELEASE_X86_64 x86_64

GNU Core Utilitiesの中でもソースが分かり易いコマンドなので今回, 取り上げてみました。

コマンドオプションのParseの仕組み

unameのソースコードを見る前に, コマンド入力時にプログラムがどのようにオプションをparseしているかを, 簡単なコマンドの作成を通じて理解します。

// printopt.c
#include 
#include 

int main (int argc, char **argv)
{
  int c;

  while ((c = getopt (argc, argv, "ab:c")) != -1)
        {
          switch (c)
            {
            case 'a':
              printf("%c option\n", optopt);
              break;
            case 'b':
              printf("%c option  :argv[%d] (optval : %s) \n", optopt, optind-1, optarg);
              break;
            case 'c':
              printf("%c option\n", optopt);
            break;
            default:
              printf("invalid option\n");
              break;
            }
        }    
  return 0;

}

getoptはコマンドラインオプションインターフェイスです。main関数に渡された int argc(argment count), char* argv(argment variable)を渡すことで-で始めるオプション要素を返してくれます。

Objective-Cの main.m における argc, argv についても同様です。

// main.m
int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

argcでコマンド名を含めた文字列数が取得でき, またダブルポインタ **argvで分割された各文字列の先頭アドレスにはある文字を取得できます。
例えば上記のprintoptコマンドでは, argv[0]にコマンド名, argv[1]以降でコマンドのオプションが入るイメージです。
また, optargでoptionのvalueを取得できます。

では, このオプションを処理するだけのプログラムをBuildします。

$ gcc -o printopt printopt.c 

実行してみます。

$ ./printopt -a -b:1
a option
b option  :argv[2] (optval : :1) 
$ ./printopt -a -b:1 -c -p
a option
b option  :argv[2] (optval : :1) 
c option
./printopt: illegal option -- p
invalid option

続いて本題です。

unameソースコードリーディング

GNU実装版のソースコードは以下で取得できます。

$ git clone git://git.sv.gnu.org/coreutils.git

unameコマンドをみていきます。まずはmain関数からです。

argv[0]をset_program_nameに渡している辺りはこれまでの話から理解できると思います。
また, このset_program_nameでグローバル変数に設定します。

int
main (int argc, char **argv)
{
  static char const unknown[] = "unknown";

  /* Mask indicating which elements to print. */
  unsigned int toprint = 0;

  initialize_main (&argc, &argv);
  set_program_name (argv[0]);
  setlocale (LC_ALL, "");
  bindtextdomain (PACKAGE, LOCALEDIR);
  textdomain (PACKAGE);

  atexit (close_stdout);

  toprint = decode_switches (argc, argv);

  if (toprint == 0)
    toprint = PRINT_KERNEL_NAME;

  if (toprint
       & (PRINT_KERNEL_NAME | PRINT_NODENAME | PRINT_KERNEL_RELEASE
          | PRINT_KERNEL_VERSION | PRINT_MACHINE))
    {
      struct utsname name;

      if (uname (&name) == -1)
        error (EXIT_FAILURE, errno, _("cannot get system name"));

      if (toprint & PRINT_KERNEL_NAME)
        print_element (name.sysname);
      if (toprint & PRINT_NODENAME)
        print_element (name.nodename);
      if (toprint & PRINT_KERNEL_RELEASE)
        print_element (name.release);
      if (toprint & PRINT_KERNEL_VERSION)
        print_element (name.version);
      if (toprint & PRINT_MACHINE)
        print_element (name.machine);
    }

  if (toprint & PRINT_PROCESSOR)
    {
      char const *element = unknown;
#if HAVE_SYSINFO && defined SI_ARCHITECTURE
      {
        static char processor[257];
        if (0 <= sysinfo (SI_ARCHITECTURE, processor, sizeof processor))
          element = processor;
      }
#endif
#ifdef UNAME_PROCESSOR
      if (element == unknown)
        {
          static char processor[257];
          size_t s = sizeof processor;
          static int mib[] = { CTL_HW, UNAME_PROCESSOR };
          if (sysctl (mib, 2, processor, &s, 0, 0) >= 0)
            element = processor;

# ifdef __APPLE__
          /* This kludge works around a bug in Mac OS X.  */
          if (element == unknown)
            {
              cpu_type_t cputype;
              size_t s = sizeof cputype;
              NXArchInfo const *ai;
              if (sysctlbyname ("hw.cputype", &cputype, &s, NULL, 0) == 0
                  && (ai = NXGetArchInfoFromCpuType (cputype,
                                                     CPU_SUBTYPE_MULTIPLE))
                  != NULL)
                element = ai->name;

              /* Hack "safely" around the ppc vs. powerpc return value. */
              if (cputype == CPU_TYPE_POWERPC
                  && STRNCMP_LIT (element, "ppc") == 0)
                element = "powerpc";
            }
# endif
        }
#endif
      if (! (toprint == UINT_MAX && element == unknown))
        print_element (element);
    }

  if (toprint & PRINT_HARDWARE_PLATFORM)
    {
      char const *element = unknown;
#if HAVE_SYSINFO && defined SI_PLATFORM
      {
        static char hardware_platform[257];
        if (0 <= sysinfo (SI_PLATFORM,
                          hardware_platform, sizeof hardware_platform))
          element = hardware_platform;
      }
#endif
#ifdef UNAME_HARDWARE_PLATFORM
      if (element == unknown)
        {
          static char hardware_platform[257];
          size_t s = sizeof hardware_platform;
          static int mib[] = { CTL_HW, UNAME_HARDWARE_PLATFORM };
          if (sysctl (mib, 2, hardware_platform, &s, 0, 0) >= 0)
            element = hardware_platform;
        }
#endif
      if (! (toprint == UINT_MAX && element == unknown))
        print_element (element);
    }

  if (toprint & PRINT_OPERATING_SYSTEM)
    print_element (HOST_OPERATING_SYSTEM);

  putchar ('\n');

  exit (EXIT_SUCCESS);
}

まず, unameがシステムの情報を取得するために必要なutsnameライブラリをincludeしています。

#include <sys/utsname.h>

utsname.hで実際の環境のカーネル情報を取得するため, utsname構造体が定義されています。

struct utsname {
    char sysname[];    /* OS の名前 (例: "Linux") */
    char nodename[];   /* 「実装時に定義された、何らかの
                          ネットワーク」におけるマシン名 */
    char release[];    /* オペレーションシステムのリリース番号 (例: "2.6.28") */
    char version[];    /* オペレーティングシステムのバージョン */
    char machine[];    /* ハードウェア識別子 */
#ifdef _GNU_SOURCE
    char domainname[]; /* NIS や YP のドメイン名 */
#endif
};

decode_switches()が実際にオプションをparseする関数で, この関数にargc, argvを渡します。

 toprint = decode_switches (argc, argv);

whileの中で, toprintに各オプションで定義された値をbit単位の論理和を取っていき, toprintを返します。

オプションがない場合はtoprintには初期値の0が入りますが, 後にPRINT_KERNEL_NAMEをセットしているためカーネル名だけが表示されます。例えば, OSXならDarwinと表示されることになります。

static int
decode_switches (int argc, char **argv)
{
  int c;
  unsigned int toprint = 0;

  if (uname_mode == UNAME_ARCH)
    {
      while ((c = getopt_long (argc, argv, "",
                               arch_long_options, NULL)) != -1)
        {
          switch (c)
            {
            case_GETOPT_HELP_CHAR;

            case_GETOPT_VERSION_CHAR (PROGRAM_NAME, ARCH_AUTHORS);

            default:
              usage (EXIT_FAILURE);
            }
        }
      toprint = PRINT_MACHINE;
    }
  else
    {
      while ((c = getopt_long (argc, argv, "asnrvmpio",
                               uname_long_options, NULL)) != -1)
        {
          switch (c)
            {
            case 'a':
              toprint = UINT_MAX;
              break;

            case 's':
              toprint |= PRINT_KERNEL_NAME;
              break;

            case 'n':
              toprint |= PRINT_NODENAME;
              break;

            case 'r':
              toprint |= PRINT_KERNEL_RELEASE;
              break;

            case 'v':
              toprint |= PRINT_KERNEL_VERSION;
              break;

            case 'm':
              toprint |= PRINT_MACHINE;
              break;

            case 'p':
              toprint |= PRINT_PROCESSOR;
              break;

            case 'i':
              toprint |= PRINT_HARDWARE_PLATFORM;
              break;

            case 'o':
              toprint |= PRINT_OPERATING_SYSTEM;
              break;

            case_GETOPT_HELP_CHAR;

            case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);

            default:
              usage (EXIT_FAILURE);
            }
        }
    }

  if (argc != optind)
    {
      error (0, 0, _("extra operand %s"), quote (argv[optind]));
      usage (EXIT_FAILURE);
    }

  return toprint;
}

getopt_longはgetoptの拡張版で, 以下の構造体を渡すことで長いオプション名と短いオプション名の対応付けを行います。

static struct option const uname_long_options[] =
{
  {"all", no_argument, NULL, 'a'},
  {"kernel-name", no_argument, NULL, 's'},
  {"sysname", no_argument, NULL, 's'},	/* Obsolescent.  */
  {"nodename", no_argument, NULL, 'n'},
  {"kernel-release", no_argument, NULL, 'r'},
  {"release", no_argument, NULL, 'r'},  /* Obsolescent.  */
  {"kernel-version", no_argument, NULL, 'v'},
  {"machine", no_argument, NULL, 'm'},
  {"processor", no_argument, NULL, 'p'},
  {"hardware-platform", no_argument, NULL, 'i'},
  {"operating-system", no_argument, NULL, 'o'},
  {GETOPT_HELP_OPTION_DECL},
  {GETOPT_VERSION_OPTION_DECL},
  {NULL, 0, NULL, 0}
};

main関数に戻り, toprintと各オプションとのbit単位の論理積をチェックし, 1であればprint_elementを呼びます。

if (toprint & PRINT_KERNEL_NAME)
        print_element (name.sysname);
      if (toprint & PRINT_NODENAME)
        print_element (name.nodename);
      if (toprint & PRINT_KERNEL_RELEASE)
        print_element (name.release);
      if (toprint & PRINT_KERNEL_VERSION)
        print_element (name.version);
      if (toprint & PRINT_MACHINE)
        print_element (name.machine);

該当したものに対して, print_element関数内のfputsで標準出力に出力します。

static void
print_element (char const *element)
{
  static bool printed;
  if (printed)
    putchar (' ');
  printed = true;
  fputs (element, stdout);
}

PRINT_PROCESSORについては, #ifdefディレクティブでOSごとに処理を切り替えています。
Linuxであれば, sysinfo(2)システムコールで, システム全体の統計情報を返します。同様の操作はOSXの場合, sysctlbyname()です。

以上の流れで, 最後に改行コードのputchar (‘\n’)を出力した後, exit(3)でプログラムを終了させています。
exitの引数を0, -1といった数値ではなく, EXIT_SUCCESS, EXIT_FAILUREを渡すことで異なるプラットフォームへの移植性を容易にしているようです。