起因

闲来无事时通常会打开 github/trending 看看当前大家热门的项目是什么,同时也希望看到自己感兴趣的项目,以此来提高自己。这天,突然看到了一个名为《程序员应该访问的最佳网站中文版》,里面有一 part 是《bash和bash脚本》,这种程序员+生产力相关内容是我最感兴趣的类目之一了,点进去浏览了一会。突然一个历史遗留的模糊问题又浮现在了我的脑海:shell 和 bash 是什么关系?。虽说之前也曾整理过这个问题,但是禁不住时间的洗礼,这不又得百度一下。搜到了《Shell是什么?1分钟理解Shell的概念! 》,里面有句话:

我们运行一个命令,大部分情况下 Shell 都会去调用内核暴露出来的接口,这就是在使用内核。接口其实就是一个一个的函数,使用内核就是调用这些函数。这就是使用内核的全部内容了吗?嗯,是的!除了函数,你没有别的途径使用内核。

这句话对我来说太有用了。简直是盘古开天辟地,把我脑海中的混沌,一下辟成两半,清澈起来了。以至于我还哪管什么是 shell,什么是 bash,赶紧借这句话进一步理清一下内核、系统调用、shell 等之间的关系。

man 手册

从哪开始理呢?首先想到的是 man 手册,man 手册共分 8 部分,如下:

1  Executable programs or shell commands
2  System calls (functions provided by the kernel)
3  Library calls (functions within program libraries)
4  Special files (usually found in /dev)
5  File formats and conventions eg /etc/passwd
6  Games
7  Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
8  System administration commands (usually only for root)

审视了十秒钟,感觉哪里不对劲,突然恍然大悟,怎么把 可执行程序和 shell 命令 放在了第一页,应该把 系统调用 放在第一页才符合从底层到上层的排列顺序嘛,怪不得我之前用了好久才记住 1 2 3 分别代表哪部分的内容,原来是不符合习惯呀(终于找到借口了)。为了确认自己的观点,打开了《UNIX环境高级编程第二版》,UNIX 操作系统的体系结构图了然如下:

unix体系结构

结合这张图,所以我建议 man 手册 的前 3 部分排序改为如下:

1  System calls (functions provided by the kernel)
2  Library calls (functions within program libraries)
3  Executable programs or shell commands

即:

1  系统调用(内核提供的函数接口)
2  库函数 (建立在系统调用之上的公用函数库,应用软件既可以使用公用函数库,也可以使用系统调用)
3  可执行程序或 shell 命令

怀疑 UNIX 操作系统体系结构的准确性

自己私自调整 man 手册 的前 3 部分排序后,心中不由窃喜。突然,又有一个细节引起了我的思考,为什么 可执行程序或 shell 命令 要排在 库函数 的后面,这样排序得依据 shell 命令必须使用了库函数 这个理由才行,再仔细看看上面那张 UNIX 的体系结构图,貌似 shell库函数 是平级的。并没有调用关系,难道事实真的是这样吗?

源码验证

下载 coreutils-5.0 源码。其实看 busybox 源码也一样,不过个人感觉 coreutils 更经典一些。

coreutils 是GNU下的一个软件包,包含linux下的 ls 等常用命令。 —— 百度百科

随便找一个,就看我们最熟悉的 ls 吧,打开 ls.c,部分代码如下(仅保留我们关心的部分):

/* Read directory `name', and list the files in it.
   If `realname' is nonzero, print its name instead of `name';
   this is used for symbolic links to directories. */

static void
print_dir(const char *name, const char *realname)
{
...
  dirp = opendir(name);
  if (!dirp)
  {
    error(0, errno, "%s", quotearg_colon(name));
    exit_status = 1;
    return;
  }

  if (LOOP_DETECT)
  {
    struct stat dir_stat;
    int fd = dirfd(dirp);

    /* If dirfd failed, endure the overhead of using stat.  */
    if ((0 <= fd
             ? fstat(fd, &dir_stat)
             : stat(name, &dir_stat)) < 0)
    {
      error(0, errno, _("cannot determine device and inode of %s"),
            quotearg_colon(name));
      exit_status = 1;
      return;
    }


    DEV_INO_PUSH(dir_stat.st_dev, dir_stat.st_ino);
  }

...

  if (files_index)
    print_current_files();
}

int
main (int argc, char **argv)
{
  ...
  while (pending_dirs)
  {
    thispend = pending_dirs;
    pending_dirs = pending_dirs->next;

    if (LOOP_DETECT)
    {
      if (thispend->name == NULL)
      {
        /* thispend->name == NULL means this is a marker entry
		 indicating we've finished processing the directory.
		 Use its dev/ino numbers to remove the corresponding
		 entry from the active_dir_set hash table.  */
        struct dev_ino di = dev_ino_pop();
        struct dev_ino *found = hash_delete(active_dir_set, &di);
        /* ASSERT_MATCHING_DEV_INO (thispend->realname, di); */
        assert(found);
        dev_ino_free(found);
        free_pending_ent(thispend);
        continue;
      }
    }

    print_dir(thispend->name, thispend->realname);

    free_pending_ent(thispend);
    print_dir_name = 1;
  }
  ...
  exit (exit_status);
}

可以看到,ls 命令的源码中,

  • 既包含 系统调用 ,如:stat()、fstat(),所在头文件 <sys/stat.h>
  • 又包含 库函数,如:opendir(),所在头文件 <dirent.h>

UNIX 操作系统体系结构调整

所以,我又发现了《UNIX 环境高级编程第二版》中的一处不合理的地方(个人认为)。遂将结构更改为如下:

UNIX操作系统结构调整