有时候,Linux® 中的文件系统是一个相当简单的树。进程可以对本身执行 chroot()
,使其文件系统树的根成为系统文件系统根的一个子目录。在树中的任何节点上,可以用来自新设备的树覆盖文件系统。
在 2000 年,Al Viro 为 Linux 引入了绑定挂载和文件系统名称空间:
- 绑定挂载(bind mount)允许从任何其他位置访问任何文件或目录。
- 文件系统名称空间(filesystem namespace)是与不同进程相关联的完全独立的文件系统树。
在执行 clone(2)
时,进程请求它当前的文件系统树的拷贝(更多信息见 参考资料);在此之后,新进程就拥有与原进程的文件系统树相同的拷贝。在建立拷贝之后,在这两个树中的任何挂载操作都不会影响另一个拷贝。
尽管每个进程使用单独的文件系统名称空间在理论上非常有意义,但是在实践中,完全隔离它们会造成较大的限制性。进程克隆了系统的文件系统名称空间之后,已经运行的系统守护进程无法为这个用户自动挂载 CD-ROM,因为在原文件系统名称空间中执行的挂载无法影响用户的拷贝。
2006 年引入的挂载传播(mount propagation)解决了这个问题,挂载传播定义了挂载对象之间的关系。系统用这些关系决定任何挂载对象中的挂载事件如何传播到其他挂载对象:
- 如果两个挂载对象具有共享关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,反之亦然。
- 如果两个挂载对象形成从属(slave)关系,那么一个挂载对象中的挂载事件会传播到另一个挂载对象,但是反过来不行;在这种关系中,从属对象是事件的接收者。
传播事件的挂载对象称为共享挂载(shared mount);接收挂载事件的挂载对象称为从属挂载(slave mount)。既不传播也不接收挂载事件的挂载对象称为私有挂载(private mount)。另一种特殊的挂载对象称为不可绑定的挂载(unbindable mount),它们与私有挂载相似,但是不允许执行绑定挂载。不可绑定的挂载对于快速增长挂载对象尤其有意义(后面会进一步讨论这个概念)。
在默认情况下,所有挂载都是私有的。可以用以下命令将挂载对象显式地标为共享挂载:
mount --make-shared <mount-object>
例如,如果 /
上的挂载必须是共享的,那么执行以下命令:
mount --make-shared /
从共享挂载克隆的挂载对象也是共享的挂载;它们相互传播挂载事件。
通过执行以下命令,可以显式地将一个共享挂载转换为从属挂载:
mount --make-slave <shared-mount-object>
从从属挂载克隆的挂载对象也是从属的挂载,它也从属于原来的从属挂载的主挂载对象。
通过执行以下命令,可以将挂载对象标为私有的:
mount --make-private <mount-object>
通过执行以下命令,可以将挂载对象标为不可绑定的:
mount --make-unbindable <mount-object>
最后,这些设置都可以递归地应用,这意味着它们将应用于目标挂载之下的所有挂载。
例如:
mount --make-rshared /
将 /
之下的所有挂载转换为共享挂载。
每个登录专用的名称空间
清单 1 给出一个 PAM(pluggable authentication module,可插入身份验证模块)的部分代码,它将除根用户之外的每个用户放在一个私有名称空间中。如果 /tmp/priv/USER 目录存在,那么这个目录将被绑定挂载在用户的私有名称空间中的 /tmp 上。
清单 1. 实现每个登录专用的名称空间的 PAM 代码片段
#define DIRNAMSZ 200
int handle_login(const char *user)
{
int ret = 0;
struct stat statbuf;
char dirnam[DIRNAMSZ];
if (strcmp(user, "root") == 0)
return PAM_SUCCESS;
ret = unshare(CLONE_NEWNS);
if (ret) {
mysyslog(LOG_ERR, "failed to unshare mounts for %s\n", user);
return PAM_SESSION_ERR;
}
snprintf(dirnam, DIRNAMSZ, "/tmp/priv/%s", user);
ret = stat(dirnam, &statbuf);
if (ret == 0 && S_ISDIR(statbuf.st_mode)) {
ret = mount(dirnam, "/tmp", "none", MS_BIND, NULL);
if (ret) {
mysyslog(LOG_ERR, "failed to mount tmp for %s\n", user);
return PAM_SESSION_ERR;
}
} else
mysyslog(LOG_INFO, "No private /tmp for user %s\n", user);
return PAM_SUCCESS;
}
int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
const char **argv)
{
const char *PAM_user = NULL;
char *fnam;
int ret;
ret = pam_get_user(pamh, &PAM_user, NULL);
if (ret != PAM_SUCCESS) {
mysyslog(LOG_ERR, "PAM-NS: couldn't get user\n");
return PAM_SESSION_ERR;
}
return handle_login(PAM_user);
}
要想使用这个 PAM 模块,可以从后面的 下载 一节下载完整的 pam_ns.c 文件和对应的 makefile。编译它并将生成的 pam_ns.so 文件复制到 /lib/security/ 中。然后在 /etc/pam.d/login 和 /etc/pam.d/sshd 中添加以下条目:
session required pam_ns.so
最后,为用户 USER
创建一个私有的 tmp 目录。
mkdir /tmp/priv
chmod 000 /tmp/priv
mkdir /tmp/priv/USER
chown -R USER /tmp/priv/USER
现在,以根用户身份在一个终端上登录,以 USER
用户身份在另一个终端上登录。作为 USER
执行以下命令:
touch /tmp/ab
ls /tmp
注意,USER
的 /tmp 只包含新创建的文件。
接下来,在根用户的终端上列出 /tmp 的内容清单;注意,这里有其他文件,但是没有 /tmp/ab。这些 /tmp 目录实际上是单独的目录。要想在根用户的终端*问 USER
的 /tmp 目录,应该执行:
ls /tmp/priv/USER
这时会看到文件 ab。接下来,在根用户的终端上,在 /mnt 上挂载某些东西:
mount --bind /dev /mnt
注意,在根用户的终端上,/dev 的内容出现在 /mnt 下面,但是在 USER
的终端上没有出现。这两个终端的挂载树是完全独立的。可以使用 mount(8)
命令获得与挂载传播相关的指令。在默认情况下,所有挂载都是私有的。所以,在作为 USER
登录之前,可以执行以下命令:
mount --make-rshared /
在此之后,挂载事件就会在后面的非共享名称空间之间传播。但是,在 USER
登录之后,将 /tmp/priv/USER 挂载到 /tmp 的事件不应该传播到父名称空间。为了解决这个问题,pam_ns.so 可以将它的文件系统标为从属对象,见清单 2。
清单 2. 将用户的名称空间标为从属对象的 PAM 模块
#define DIRNAMSZ 200
#ifndef MS_SLAVE
#define MS_SLAVE 1<<19
#endif
#ifndef MS_REC
#define MS_REC 0x4000
#endif
int handle_login(const char *user)
{
int ret = 0;
struct stat statbuf;
char dirnam[DIRNAMSZ];
if (strcmp(user, "root") == 0)
return PAM_SUCCESS;
ret = unshare(CLONE_NEWNS);
if (ret) {
mysyslog(LOG_ERR, "failed to unshare mounts for %s\n", user);
return PAM_SESSION_ERR;
}
ret = mount("", "/", "dontcare", MS_REC|MS_SLAVE, ""));
if (ret) {
mysyslog(LOG_ERR, "failed to mark / rslave for %s\n", user);
return PAM_SESSION_ERR;
}
snprintf(dirnam, DIRNAMSZ, "/tmp/priv/%s", user);
ret = stat(dirnam, &statbuf);
if (ret == 0 && S_ISDIR(statbuf.st_mode)) {
ret = mount(dirnam, "/tmp", "none", MS_BIND, NULL);
if (ret) {
mysyslog(LOG_ERR, "failed to mount tmp for %s\n", user);
return PAM_SESSION_ERR;
}
} else
mysyslog(LOG_INFO, "No private /tmp for user %s\n", user);
return PAM_SUCCESS;
}
每个用户专用的根
在 每个登录专用的名称空间 一节中,我们看到了一个通过挂载名称空间为用户提供私有名称空间的简单示例。通过使用挂载传播,这个解决方案非常适合为用户提供私有的 /tmp 目录;如果再添加一个由 pam_ns.c 解析的配置文件,就可以根据用户对其他目录进行重定向。LSPP 就是以这种方式提供多个实例化的主目录,它会根据登录过程的许可在 /home/USER 上挂载不同的目录。
但是,同一用户的每次登录都会获得一个私有的从属文件系统。所以这个用户在一个登录会话中执行的挂载不影响另一个登录会话。
非管理员用户可以以几种方式挂载文件系统。例如,用户可以使用 FUSE 挂载 sshfs(安全 shell)文件系统或 loopback 文件系统(见 参考资料)。如果不考虑本文后面讨论的用户之间的挂载共享问题,就不容易理解这些挂载如何只出现在用户的一个登录终端中,而不出现在其他终端中。但是,通过使用 “每个登录专用的名称空间” 一节中的方法,确实可以实现这种效果。
清单 3 给出 pam_chroot.so PAM 模块的部分代码。pam_ns.so 在登录时克隆挂载名称空间,但是 pam_chroot.so 模块需要在 /share/USER/root 下设置一个用户专用的文件系统,并使用 chroot()
将用户限制在他的私有文件系统中。
清单 3. 使用 chroot() 的 PAM 模块
int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
const char **argv)
{
const char *PAM_user = NULL;
char fnam[400];
int ret, err, count, i;
struct mount_entries *entries;
struct stat statbuf;
ret = pam_get_user(pamh, &PAM_user, NULL);
if (ret != PAM_SUCCESS) {
mysyslog(LOG_ERR, "PAM-MOUNT: couldn't get user\n");
return PAM_SESSION_ERR;
}
/* check whether /share/$pam_user/root exists. If so, chroot to it */
sprintf(fnam, "/share/%s/root", PAM_user);
ret = stat(fnam, &statbuf);
if (ret == 0 && S_ISDIR(statbuf.st_mode)) {
ret = chroot(fnam);
if (ret) {
mysyslog(LOG_ERR, "PAM-MOUNT: unable to chroot to %s\n", fnam);
return PAM_SESSION_ERR;
}
}
return PAM_SUCCESS;
}
在这个示例中,在系统启动时提前执行所有挂载。例如,在启动之后,执行:
mkdir -p /share/USER/root
mount --make-rshared /
mount --rbind / /share/USER/root
mount --make-rslave /share/USER/root
mount --bind /share/USER/root/tmp/priv/USER /share/USER/root/tmp
这里没有使用私有名称空间。相反,USER
的每次登录都通过 chroot()
限制在同一个目录(/share/USER/root)中。因此,在 USER
的所有登录中,会看到他的任何登录所执行的任何挂载。而 OTHERUSER
通过 chroot()
限制在 /share/OTHERUSER/root 中,因此不会看到 USER
的挂载活动。
这种方法的一个缺点是,可以回避普通的 chroot()
机制(虽然这需要某些特权)。例如,在具有某些特权(包括 CAP_SYS_CHROOT)的情况下,程序可以回避 chroot()
(参见 参考资料),这会导致程序能够访问真正的文件系统根。根据使用用户专用文件系统树的实际意图的不同,这可能会成为一个问题。
解决这个问题的方法是,在私有名称空间中使用 pivot_root(2)
(而不是 chroot(2)
)将登录的根目录改为 /share/USER/root。chroot()
仅仅将进程的文件系统根指向一个指定的新目录,而 pivot_root()
会使指定的 new_root 目录(它必须是一个挂载)脱离它的挂载点,然后将它附着到进程的根目录。因为在挂载树中新的根目录没有父目录,所以无法像使用 chroot()
时那样欺骗系统,从而访问真正的文件系统根。我们将使用 pivot_root()
方式。
用户专用根目录的系统设置
您已经看到了用户专用私有挂载树的实现细节,包括在登录时必须执行的操作。在本节中,将看到在创建用户帐户和系统引导时使用的完整脚本。
清单 4 给出在创建用户时运行的脚本。
清单 4. 用于创建用户的脚本
create_user_tree {
user = $1
mkdir /user/$user
mount --rbind / /user/$user
mount --make-rslave /user/$user
mount --make-rshared /user/$user
#create a private mount. This is to facilitate pivot_root
#to temporarily place the old root here before detaching the
#entire old root tree. NOTE: pivot_root will not allow old root
#to be placed under a shared mount.
pushd /user/$user/
mkdir -p __my_private_mnt__
mount --bind __my_private_mnt__ __my_private_mnt__
mount --make-private __my_private_mnt__
popd
}
这个脚本假设已经运行了 init_per_user_namespace 脚本(后面会讨论这个脚本)。它在 /user/ 下面为用户帐户创建一个目录。然后将根目录递归地绑定挂载在 /user/$user/ 下面。这个递归复制的根文件系统树成为这个用户专用的文件系统,可以持久地保留这个用户执行的挂载活动(对多次登录有效,但是在重新启动之后就会失效)。
这个复制的树是根树的从属挂载,所以根树中的挂载活动会传播到这个拷贝,但是不会反向传播。这个树被标为共享的,所以后续的拷贝(也就是通过名称空间克隆建立的拷贝)是相互共享的挂载;任何拷贝中的挂载活动都会传播到所有其他拷贝。
最后,创建一个私有的挂载 __my_private_mnt__。这是为了帮助 pivot_root()
(见清单 6)在删除树之前临时准备根挂载。目前不需要太关注它。了解了 pivot_root()
的语义之后,这个步骤的意义就会明确了。目前只需记住,如果准备的挂载是共享的,那么 pivot 挂载就不会成功。
清单 5 给出在系统引导时运行的脚本。
清单 5. 在引导时执行系统初始化的脚本
init_per_user_namespace {
#start with a clean state by marking
#all mounts as private.
mount --make-rprivate /
#create a unbindable mount called 'user'
#and have all the users to bind the entire
#system tree '/' under them.
mkdir /user
mount --bind /user /user
mount --make-rshared /
mount --make-unbindable /user
foreach user in existing_user {
create_user_tree $user
}
}
它创建 /user 目录,用户专用的挂载树将放在这个目录中。然后,它将 /user 绑定挂载到本身。--rshared
等挂载传播指令只能针对挂载点指定。这个步骤确保在 /user 上存在挂载点。
接下来,将文件系统根标为 --rshared
,这样的话以后的拷贝(包括通过绑定挂载或克隆挂载名称空间创建的拷贝)都与这个挂载相互共享,任何树中的挂载操作都会传播到所有共享挂载。
接下来,将 /user 上的挂载标为不可绑定的。对于每个用户,都会递归地复制整个挂载树,所以在 /user/$user_1 下面创建第一个用户的拷贝之后,在 /user/$user_2 下面创建的拷贝会包含 /user/$user_1 的递归拷贝(/user/$user_2/user/$user_1)。可以想像到,这会快速地消耗大量内存。将 /user 标为不可绑定的,就可以防止在递归地绑定挂载 / 时复制 /user。
最后,为每个用户执行一次 清单 4 中的脚本。如果 /user/$user 目录不存在,就创建这个目录,并按照前面的描述设置适当的挂载传播。
清单 6 给出在用户登录时执行的 PAM 模块的片段。
清单 6. 用户登录所用的 PAM 代码片段
#ifndef MNT_DETACH
#define MNT_DETACH0x0000002
#endif
#ifndef MS_REC
#define MS_REC0x4000
#endif
#ifndef MS_PRIVATE
#define MS_PRIVATE 1<<18 /* Private */
#endif
#define DIRNAMSZ 200
int handle_login(const char *user)
{
int ret = 0;
struct stat statbuf;
char dirnam[DIRNAMSZ], oldroot[DIRNAMSZ];
snprintf(dirnam, DIRNAMSZ, "/user/%s", user);
ret = stat(dirnam, &statbuf);
if (ret != 0 || !S_ISDIR(statbuf.st_mode))
return PAM_SUCCESS;
ret = unshare(CLONE_NEWNS);
if (ret) {
mysyslog(LOG_ERR, "failed to unshare mounts for %s, error %d\n",
user, errno);
return PAM_SESSION_ERR;
}
ret = chdir(dirnam);
if (ret) {
mysyslog(LOG_ERR, "failed to unshare mounts for %s, error %d\n",
user, errno);
return PAM_SESSION_ERR;
}
snprintf(oldroot, DIRNAMSZ, "%s/__my_private_mnt__", dirnam);
ret = pivot_root(dirnam, oldroot);
if (ret) {
mysyslog(LOG_ERR, "failed to pivot_root for %s, error %d\n",
user, errno);
mysyslog(LOG_ERR, "pivot_root was (%s,%s)\n", dirnam, oldroot);
return PAM_SESSION_ERR;
}
ret = mount("", "/__my_private_mnt__", "dontcare", MS_REC|MS_PRIVATE, "");
if (ret) {
mysyslog(LOG_ERR, "failed to mark /tmp private for %s, error %d\n",
user, errno);
return PAM_SESSION_ERR;
}
ret = umount2("/__my_private_mnt__", MNT_DETACH);
if (ret) {
mysyslog(LOG_ERR, "failed to umount old_root %s, error %d\n",
user, ret);
return PAM_SESSION_ERR;
}
return PAM_SUCCESS;
}
这个模块首先检查正在登录的用户的 /user/USER 树是否存在。如果这个树不存在,那么这个模块仅仅允许这个用户登录,而不执行任何其他操作。
如果 /user/USER 树存在,那么第一步是为这个登录进程下的任务克隆一个私有的名称空间。因此,这些进程会有自己的系统初始挂载树拷贝。但是,这些拷贝互不相连;复制的树中的每个挂载节点共享初始节点中对应的挂载节点。
接下来,登录进程使用 pivot_root()
将它的文件系统根改为 /user/$user。原来的根被挂载在新的 __my_private_mnt__ 下面。
下一步是将 __my_private_mnt__ 标为私有的,使后面的卸载操作不会传播到根挂载树的其他拷贝,包括原来的树。
最后,从 __my_private_mnt__ 卸载原来的根。
在用户创建脚本(见 清单 4 )中,将 __my_private_mnt__ 目录设置为私有的挂载,并指出这是为了辅助 pivot_root()
的操作。这么做实际上是由于 pivot_root()
有一个文档中没有记载的限制,这个限制与旧根和新根的挂载传播状态有关。要想让 pivot_root()
成功执行,以下挂载不能是共享对象:
- 旧根的目标位置
- 新根的当前父位置(在调用
pivot_root()
的时候) - 新根的目标父位置
在 清单 4 中接近末尾的地方将 __my_private_mnt__ 标为私有的,就可以满足第一个条件。第二个条件已经满足了,因为新根的当前父位置是 /user,/user 上的挂载是一个不可绑定的挂载。第三个条件也已经满足了,因为新根的目标父位置就是当前根的父位置。这个挂载是一个不可见的 rootfs 挂载,它已经是私有的。
在本节中,讨论了如何实现每个用户专用的挂载树,让挂载事件在一个用户的所有登录会话之间共享,但是对其他用户隐藏。在下一节中,讨论如何允许用户相互共享挂载树。
可以选择性共享每个用户专用的挂载树
我们已经讨论了每个用户专用的挂载树;现在讨论如何允许用户选择一部分挂载树在用户之间共享。下面是一个用于系统引导的脚本。
清单 7. 用于系统引导的脚本
init_per_user_namespace {
mkdir -p /user/slave_tree
mkdir -p /user/share_tree
#start with a clean state. Set all mounts to private.
mount --make-rprivate /
mount --bind /user /user
mount --bind /user/share_tree /user/share_tree
mount --bind /user/slave_tree /user/slave_tree
mount --make-rshared /
mount --make-unbindable /user
for user in `cat /etc/user_list`; do
sh /bin/create_user_tree $user
done
}
首先创建一个 /user 挂载,它包含每个用户的根目录。然后,在 /user 下面创建另一个称为 /user/share_tree 的目录,其中包含每个用户可以与其他用户共享的挂载。还创建一个 /user/slave_tree 目录,其中的挂载由每个用户共享,但是不接受来自其他用户的任何修改。当然,为了包含非绑定挂载,我们将 /user 下面的挂载标为不可绑定的。最后,调用 create_user_tree 为每个用户创建挂载树。
清单 8 给出了创建挂载树并允许其他用户共享挂载涉及的步骤。
清单 8. 用于创建用户的脚本
create_user_tree {
user = $1
mkdir -p /user/$user
#copy over the entire mount tree under /user/$user
mount --rbind / /user/$user
make --make-rslave /user/$user
make --make-rshared /user/$user
cd /user/$user/home/$user
#export my shared exports
mkdir -p my_shared_exports
chown $user my_shared_exports
mount --bind my_shared_exports my_shared_exports
mount --make-private my_shared_exports
mount --make-shared my_shared_exports
mkdir -p /user/share_tree/$user
mount --bind my_shared_exports /user/share_tree/$user
#export my slave exports
mkdir -p my_slave_exports
chown $user my_slave_exports
mount --bind my_slave_exports my_slave_exports
mount --make-private my_slave_exports
mount --make-shared my_slave_exports
mkdir -p /user/slave_tree/$user
mount --bind my_slave_exports /user/slave_tree/$user
cd /user/$user
#import everybody's shared exports
mkdir -p others_shared_exports
mount --rbind /user/share_tree others_shared_exports
#import everybody's slave exports
mkdir -p others_slave_exports
mount --rbind /user/slave_tree others_slave_exports
mount --make-rslave others_slave_exports
#setup a private mount in the user's tree, This is to facilitate
# pivot_mount executed later, during new user-logins.
mkdir -p __my_private_mnt__
mount --bind __my_private_mnt__ __my_private_mnt__
mount --make-private __my_private_mnt__
}
首先,在 /user/$user 下复制整个挂载树。在用户的树中,创建一个共享的挂载 my_shared_exports,在 /user/share_tree/$user 下复制它,从而导出给所有用户。同样,在用户的树中创建 my_slave_exports,在 /user/slave_tree/$user 下复制它,从而导出给所有用户。这里的关键思想是,用户可以选择在 my_shared_tree 下挂载的内容,所有其他用户会自动地共享这些挂载。
接下来,复制 /user/share_tree 下的挂载树并将它挂载在正在登录的用户的 others_shared_exports 下,这样就导入了所有其他用户的共享挂载。同样,复制 /user/slave_tree 下的挂载树并将它挂载在 others_slave_exports 下,这样就导入了所有其他用户的从属挂载。当然,因为这些挂载由导出者以从属挂载的形式导出,所以我们将它们转换为从属挂载。
完成了实现共享所需的设置步骤之后,用户登录算法就与 清单 6 中一样了。在每次登录时,用户会获得一个完全相同的挂载树;同时,用户会在 /others_shared_export 和 /others_slave_exports 下分别看到其他所有用户导出的所有共享挂载和从属挂载。
如果用户希望向其他用户导出某些内容,那么只需将这些内容挂载在 my_shared_exports 下,所有用户就能够自动地看到它们。
结束语
绑定挂载可以将任何文件或目录放在任何其他目录上。名称空间允许进程克隆父进程的挂载树,建立相互隔离的拷贝。挂载传播允许文件系统树的拷贝单向或双向地共享挂载事件。这些特性让用户能够拥有接近私有的挂载树,同时使用户能够看到系统范围的挂载事件(比如 CD-ROM 挂载)并选择性地与其他用户共享自己的挂载事件。
换句话说,本文描述的挂载传播技术让用户能够建立专用的独立文件系统,以及在他们私有的文件系统树中执行导入和导出。
本文转自:http://www.ibm.com/developerworks/cn/linux/l-mount-namespaces.html