最新要闻

广告

手机

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案

家电

世界速讯:监听容器中的文件系统事件

来源:博客园

基本概念

Linux 文件系统事件监听:应用层的进程操作目录或文件时,会触发 system call,此时,内核中的 notification 子系统把该进程对文件的操作事件上报给应用层的监听进程(称为 listerner)。


(相关资料图)

dnotify:2001 年的 kernel 2.4 版本引入,只能监控 directory,采用的是 signal 机制来向 listener 发送通知,可以传递的信息很有限。

inotify:2005 年在 kernel 2.6.13 中亮相,除了可以监控目录,还可以监听普通文件,inotify 摈弃了 signal 机制,通过 event queue 向 listener 上传事件信息。

fanotify:kernel 2.6.36 引入,fanotify 的出现解决了已有实现只能 notify 的问题,允许 listener 介入并改变文件事件的行为,实现从“监听”到“监控”的跨越。

本文主要介绍如何通过 inotify 和 fanotify 监听容器中的文件系统事件。

Inotify

基本介绍

inotify(inode[1] notify)是 Linux 内核中的一个子系统,由 John McCutchan[2] 创建,用于监视文件系统事件。它可以在文件或目录发生变化时通知应用程序,例如,监听文件的创建、修改或删除事件。inotify 可以用于自动更新文件系统视图、重新加载配置文件,记录文件改变历史等场景。

Inotify 的工作流程如下:

  1. 用户通过系统调用(如:write、read)操作文件;

  2. 内核将文件系统事件保存到 fsnotify_group 的事件队列中;

  3. 唤醒等待 inotify 的进程(listener);

  4. 进程通过 fd 从内核队列读取 inotify 事件。

其中,inotify_event_info 的定义如下:

structinotify_event_info{structfsnotify_eventfse;u32mask;/*Watchmask.*/intwd;/*Watchdescriptor.*/u32sync_cookie;/*Cookietosynchronizetwoevents.*/intname_len;/*Name.*/charname[];/*Length(includingNULs)ofname.*/};

mask 标记具体的文件操作事件。

API 介绍

Inotify 可以用来监听单个文件,也可以用来监听目录。当监听的是目录时,inotify 除了生成目录的事件,还会生成目录中文件的事件。

注意:当使用 inotify 监听目录时,并不会递归监听子目录中的文件,如果需要得到这些事件,需要手动指定监听这些文件。对于很大的目录树,这个过程将花费大量时间。

参考:inotify.7[3]

  • inotify_init(void)

初始化 inotify 实例,返回文件描述符,用于内核向用户态程序传输监听到的 inotify 事件。函数声明为:

intinotify_init(void);

内核同时提供了int inotify_init1(int flags),flags 的可选值如下:

IN_NONBLOCK读取文件描述符时不会被阻塞,即使没有数据可用也是如此。如果没有数据可用,则读操作将立即返回0,而不是等待数据可用。IN_CLOEXEC如果在程序运行时打开了一个文件描述符,并且在调用execve()时没有将其关闭,那么在新程序中仍然可以使用该文件描述符。设置IN_CLOEXEC标志后,可以确保在调用execve()时关闭文件描述符,避免在新程序中使用。

可以通过 OR 指定多个flag,当flags=0等价于int inotify_init(void)

  • inotify_add_watch

添加需要监听的目录或文件(watch list),可以添加新的路径,也可以是已经添加过的路径。fdinotify_init返回的文件描述符,mask指定需要监听的事件类型,通过 OR指定多个事件。返回值是当前路径的wd(watch descriptor),可用于移除对该路径的监听。

函数声明为:

#includeintinotify_add_watch(intfd,constchar*pathname,uint32_tmask);

Inotify 支持监听的事件包括:

/*SupportedeventssuitableforMASKparameterofINOTIFY_ADD_WATCH.*/#defineIN_ACCESS0x00000001/*Filewasaccessed.*/#defineIN_MODIFY0x00000002/*Filewasmodified.*/#defineIN_ATTRIB0x00000004/*Metadatachanged.*/#defineIN_CLOSE_WRITE0x00000008/*Writtablefilewasclosed.*/#defineIN_CLOSE_NOWRITE0x00000010/*Unwrittablefileclosed.*/#defineIN_CLOSE(IN_CLOSE_WRITE|IN_CLOSE_NOWRITE)/*Close.*/#defineIN_OPEN0x00000020/*Filewasopened.*/#defineIN_MOVED_FROM0x00000040/*FilewasmovedfromX.*/#defineIN_MOVED_TO0x00000080/*FilewasmovedtoY.*/#defineIN_MOVE(IN_MOVED_FROM|IN_MOVED_TO)/*Moves.*/#defineIN_CREATE0x00000100/*Subfilewascreated.*/#defineIN_DELETE0x00000200/*Subfilewasdeleted.*/#defineIN_DELETE_SELF0x00000400/*Selfwasdeleted.*/#defineIN_MOVE_SELF0x00000800/*Selfwasmoved.*/
  • inotify_rm_watch

移除被监听的路径。fd 是inotify_init返回的文件描述符,wd 是inotify_add_watch返回的监听文件描述符。

函数声明为:

#includeintinotify_rm_watch(intfd,intwd);

实例

以下是基于 Rust 语言实现的实例:

usenix::{poll::{poll,PollFd,PollFlags},sys::inotify::{AddWatchFlags,InitFlags,Inotify,InotifyEvent},};usesignal_hook::{consts::SIGTERM,low_level::pipe};usestd::os::unix::net::UnixStream;usestd::{env,io,os::fd::AsRawFd,path::PathBuf};fnmain()->io::Result<()>{letargs:Vec=env::args().collect();ifargs.len()<2{eprintln!("Usage:{}",args[0]);std::process::exit(1);}letpath=PathBuf::from(&args[1]);//初始化inotify,得到fdletinotify_fd=Inotify::init(InitFlags::empty())?;//添加被监听的目录或文件,指定需要监听的事件letwd=inotify_fd.add_watch(&path,AddWatchFlags::IN_ACCESS|AddWatchFlags::IN_OPEN|AddWatchFlags::IN_CREATE,)?;let(read,write)=UnixStream::pair()?;//注册用于处理信号的pipeifletErr(e)=pipe::register(SIGTERM,write){println!("failedtosetSIGTERMsignalhandler{e:?}");}letmutfds=[PollFd::new(inotify_fd.as_raw_fd(),PollFlags::POLLIN),PollFd::new(read.as_raw_fd(),PollFlags::POLLIN),];loop{matchpoll(&mutfds,-1){Ok(polled_num)=>{ifpolled_num<=0{eprintln!("polled_num<=0!");break;}ifletSome(flag)=fds[0].revents(){ifflag.contains(PollFlags::POLLIN){//得到inotify事件,进行处理letevents=inotify_fd.read_events()?;foreventinevents{handle_event(event)?;}}}ifletSome(flag)=fds[1].revents(){ifflag.contains(PollFlags::POLLIN){println!("receivedSIGTERMsignal");break;}}}Err(e)=>{ife==nix::Error::EINTR{continue;}eprintln!("Pollerror{:?}",e);break;}}}inotify_fd.rm_watch(wd)?;Ok(())}fnhandle_event(event:InotifyEvent)->io::Result<()>{letfile_name=matchevent.name{Some(name)=>name,None=>returnOk(()),};letevent_mask=event.mask;letkind=ifevent_mask.contains(AddWatchFlags::IN_ISDIR){"directory"}else{"file"};println!("{}{}was{:?}.",kind,file_name.to_string_lossy(),event_mask);Ok(())}

编译&测试:

cargobuild./target/debug/inotifytest

可以看到,inotify 不会递归监听二级目录下的文件dir1/file2.txt

经测试,Inotify 可以直接监听容器 rootfs 下的目录:

nerdctlrun--rm-itgolang./target/debug/inotify/run/containerd/io.containerd.runtime.v2.task/default/CONTAINERD_ID/rootfs

Fanotify

基本介绍

Inotify 能够监听目录和文件的事件,但这种 notifiation 机制也存在局限:inotify 只能通知用户态进程触发了哪些文件系统事件,而无法进行干预,典型的应用场景是杀毒软件。

Fanotify[4] 的出现就是为了解决这个问题,同时允许递归监听目录下的子目录和文件。

Fanotify 的工作流程如下:

  1. 用户通过系统调用(如:write、read)操作文件;

  2. 内核将文件系统事件发送到 fsnotify_group 的事件队列中;

  3. 唤醒等待 fanotify 事件的进程(listener);

  4. 进程通过 fd 从内核队列读取 fanotify 事件;

  5. 如果是 FAN_OPEN_PERM 和 FAN_ACCESS_PERM 监听类型,进程需要通过 write 把许可信息(允许 or 拒绝)写回内核;

  6. 内核根据许可信息决定是否继续完成该文件系统事件。

fanotify_event 的定义如下:

structfanotify_event{structfsnotify_eventfse;structhlist_nodemerge_list;/*Listforhashedmerge*/u32mask;struct{unsignedinttype:FANOTIFY_EVENT_TYPE_BITS;unsignedinthash:FANOTIFY_EVENT_HASH_BITS;};structpid*pid;};

fsnotify_group 的定义参考:linux/fsnotify_backend.h#L185[5]

API 介绍

  • fanotify_init()

初始化 fanotify事件组,返回该事件组的文件描述符,用于内核向用户态程序传输 fanotify事件,同时接收来自用户态进程的许可信息。函数声明为:

#include/*DefinitionofO_*constants*/#includeintfanotify_init(unsignedintflags,unsignedintevent_f_flags);

flags 定义了事件通知的类型和文件描述符的行为,可选值有:

/*TheseareNOTbitwiseflags.Bothbitsareusedtogether.*/#defineFAN_CLASS_NOTIF0x00000000#defineFAN_CLASS_CONTENT0x00000004#defineFAN_CLASS_PRE_CONTENT0x00000008/*flagsusedforfanotify_init()*/#defineFAN_CLOEXEC0x00000001#defineFAN_NONBLOCK0x00000002#defineFAN_UNLIMITED_QUEUE0x00000010#defineFAN_UNLIMITED_MARKS0x00000020#defineFAN_ENABLE_AUDIT0x00000040/*Flagstodeterminefanotifyeventformat*/#defineFAN_REPORT_TID0x00000100/*event->pidisthreadid*/#defineFAN_REPORT_FID0x00000200/*Reportuniquefileid*/#defineFAN_REPORT_DIR_FID0x00000400/*Reportuniquedirectoryid*/#defineFAN_REPORT_NAME0x00000800/*Reporteventswithname*//*Conveniencemacro-FAN_REPORT_NAMErequiresFAN_REPORT_DIR_FID*/#defineFAN_REPORT_DFID_NAME(FAN_REPORT_DIR_FID|FAN_REPORT_NAME)

Fanotify 允许多个 listener 监听同一个文件系统对象,并且分为不同的级别,通过 flags指定。

  • FAN_CLASS_NOTIF:只用于监听,不访问文件内容。

  • FAN_CLASS_CONTENT:可以访问文件内容。

  • FAN_CLASS_PRE_CONTENT:在访问文件内容之前可获取访问权限。

event_f_flags用于设置 fanotify 事件创建并打开的文件描述符状态,可选值有:

#defineO_RDONLY00/*Allowonlyreadaccess.*/#defineO_WRONLY01/*Allowonlywriteaccess.*/#defineO_RDWR02/*Allowreadandwriteaccess.*///其它常用的event_f_flags#defineO_LARGEFILE__O_LARGEFILE/*Enablesupportforfilesexceeding2GB.*/#defineO_CLOEXEC__O_CLOEXEC/*Setclose_on_exec.*/

以下 event_f_flags也可以使用:O_APPENDO_DSYNCO_NOATIMEO_NONBLOCKO_SYNC,使用除这些以外的其它值将返回 EINVAL错误码。

更多请信息参考 fanotify_init.2[6]

  • fanotify_mark()

添加、删除和修改文件系统中被监听的路径,必须对该路径有访问权限。函数声明为:

#includeintfanotify_mark(intfanotify_fd,unsignedintflags,uint64_tmask,intdirfd,constchar*pathname);

fanotify_fdfanotify_init返回的文件描述符,flags描述操作类型,可选值有:

/*flagsusedforfanotify_modify_mark()*/#defineFAN_MARK_ADD0x00000001#defineFAN_MARK_REMOVE0x00000002/*如果pathname是符号链接,只监听符号链接而不需要监听文件本身(默认会监听文件本身)*/#defineFAN_MARK_DONT_FOLLOW0x00000004#defineFAN_MARK_ONLYDIR0x00000008/*只监听目录,如果传入的不是目录返回错误*//*FAN_MARK_MOUNTis0x00000010*/#defineFAN_MARK_IGNORED_MASK0x00000020#defineFAN_MARK_IGNORED_SURV_MODIFY0x00000040#defineFAN_MARK_FLUSH0x00000080/*移除所有marks*//*FAN_MARK_FILESYSTEMis0x00000100*/

mask 定义了需要监听的事件:

/*thefollowingeventsthatuser-spacecanregisterfor*/#defineFAN_ACCESS0x00000001/*Filewasaccessed*/#defineFAN_MODIFY0x00000002/*Filewasmodified*/#defineFAN_ATTRIB0x00000004/*Metadatachanged*/#defineFAN_CLOSE_WRITE0x00000008/*Writtablefileclosed*/#defineFAN_CLOSE_NOWRITE0x00000010/*Unwrittablefileclosed*/#defineFAN_OPEN0x00000020/*Filewasopened*/#defineFAN_MOVED_FROM0x00000040/*FilewasmovedfromX*/#defineFAN_MOVED_TO0x00000080/*FilewasmovedtoY*/#defineFAN_CREATE0x00000100/*Subfilewascreated*/#defineFAN_DELETE0x00000200/*Subfilewasdeleted*/#defineFAN_DELETE_SELF0x00000400/*Selfwasdeleted*/#defineFAN_MOVE_SELF0x00000800/*Selfwasmoved*/#defineFAN_OPEN_EXEC0x00001000/*Filewasopenedforexec*/#defineFAN_Q_OVERFLOW0x00004000/*Eventqueuedoverflowed*/#defineFAN_FS_ERROR0x00008000/*Filesystemerror*/#defineFAN_OPEN_PERM0x00010000/*Fileopeninpermcheck*/#defineFAN_ACCESS_PERM0x00020000/*Fileaccessedinpermcheck*/#defineFAN_OPEN_EXEC_PERM0x00040000/*Fileopen/execinpermcheck*/#defineFAN_EVENT_ON_CHILD0x08000000/*Interestedinchildevents*/#defineFAN_RENAME0x10000000/*Filewasrenamed*/#defineFAN_ONDIR0x40000000/*Eventoccurredagainstdir*//*helperevents*/#defineFAN_CLOSE(FAN_CLOSE_WRITE|FAN_CLOSE_NOWRITE)/*close*/#defineFAN_MOVE(FAN_MOVED_FROM|FAN_MOVED_TO)/*moves*/

参数 dirfdpathname确定需要监听的文件系统对象:

(1)如果 pathnameNULL,由 dirfd确定。

(2)如果 pathnameNULLdirfd的值为 AT_FDCWD,监听当前工作目录。

(3)如果 pathname为绝对路径,dirfd被忽略。

(4)如果 pathname为相对路径且 dirfd不是 AT_FDCWD,监听 pathname相对于 dirfd目录的路径。

(5)如果 pathname为相对路径且 dirfd的值为 AT_FDCWD,监听 pathname相对于当前目录的路径。

Fanotify 有 3 种监听模式:directedper-mountglobal,由 fanotify_mark函数的 flags参数指定,默认为 FAN_MARK_INODE,也就是 directed模式。

  • directedflagFAN_MARK_MOUNT,和 inotify类似,监听指定 inode对象,如果是目录,可以添加 FAN_EVENT_ON_CHILD指定监听该目录下的所有文件(不会递归监听子目录中的文件)。per-mountglobal模式下,FAN_EVENT_ON_CHILD无效。

  • per-mountflagFAN_MARK_MOUNT,监听指定挂载点下所有的内容(目录,子目录,文件),如果传入的 path不是挂载点,则会监听 path所在的挂载点。

  • globalflagFAN_MARK_FILESYSTEM,监听 path所在的文件系统,包括所有挂载点中的目录和文件。

实例

uselazy_static::lazy_static;uselibc::{__s32,__u16,__u32,__u64,__u8};usenix::poll::{poll,PollFd,PollFlags};usesignal_hook::{consts::SIGTERM,low_level::pipe};usestd::os::unix::net::UnixStream;usestd::{env,ffi,fs,io,mem,os::fd::AsRawFd,path::PathBuf,slice};#[derive(Debug,Clone,Copy)]#[repr(C)]structFanotifyEvent{event_len:__u32,vers:__u8,reserved:__u8,metadata_len:__u16,mask:__u64,fd:__s32,pid:__s32,}lazy_static!{staticrefFAN_EVENT_METADATA_LEN:usize=mem::size_of::();}constFAN_CLOEXEC:u32=0x0000_0001;constFAN_NONBLOCK:u32=0x0000_0002;constFAN_CLASS_CONTENT:u32=0x0000_0004;constO_RDONLY:u32=0;constO_LARGEFILE:u32=0;constFAN_MARK_ADD:u32=0x0000_0001;constFAN_MARK_MOUNT:u32=0x0000_0010;//constFAN_MARK_FILESYSTEM:u32=0x00000100;constFAN_ACCESS:u64=0x0000_0001;constFAN_OPEN:u64=0x0000_0020;constFAN_OPEN_EXEC:u64=0x00001000;constAT_FDCWD:i32=-100;constFAN_EVENT_ON_CHILD:u64=0x08000000;constFAN_ONDIR:u64=0x4000_0000;//初始化fanotify,调用libc的函数fninit_fanotify()->Result{unsafe{matchlibc::fanotify_init(FAN_CLOEXEC|FAN_CLASS_CONTENT|FAN_NONBLOCK,O_RDONLY|O_LARGEFILE,){-1=>Err(io::Error::last_os_error()),fd=>Ok(fd),}}}fnmark_fanotify(fd:i32,path:&str)->Result<(),io::Error>{letpath=ffi::CString::new(path)?;unsafe{matchlibc::fanotify_mark(fd,//FAN_MARK_ADD,FAN_MARK_ADD|FAN_MARK_MOUNT,//FAN_MARK_ADD|FAN_MARK_FILESYSTEM,FAN_OPEN|FAN_ACCESS|FAN_OPEN_EXEC|FAN_EVENT_ON_CHILD,AT_FDCWD,path.as_ptr(),){0=>Ok(()),_=>Err(io::Error::last_os_error()),}}}fnread_fanotify(fanotify_fd:i32)->Vec{letmutvec=Vec::new();unsafe{letbuffer=libc::malloc(*FAN_EVENT_METADATA_LEN*1024);letsizeof=libc::read(fanotify_fd,buffer,*FAN_EVENT_METADATA_LEN*1024);letsrc=slice::from_raw_parts(bufferas*mutFanotifyEvent,sizeofasusize/*FAN_EVENT_METADATA_LEN,);vec.extend_from_slice(src);libc::free(buffer);}vec}//fanotifyevent只有fd,需要手动获取对应的pathfnget_fd_path(fd:i32)->io::Result{letfd_path=format!("/proc/self/fd/{fd}");fs::read_link(fd_path)}fnmain()->io::Result<()>{letargs:Vec=env::args().collect();ifargs.len()<2{eprintln!("Usage:{}",args[0]);std::process::exit(1);}letpath_buf=PathBuf::from(&args[1]);letpath=path_buf.to_str().unwrap_or(".");letfanotify_fd=init_fanotify()?;mark_fanotify(fanotify_fd,path)?;let(read,write)=UnixStream::pair()?;ifletErr(e)=pipe::register(SIGTERM,write){println!("failedtosetSIGTERMsignalhandler{e:?}");}letmutfds=[PollFd::new(fanotify_fd.as_raw_fd(),PollFlags::POLLIN),PollFd::new(read.as_raw_fd(),PollFlags::POLLIN),];loop{matchpoll(&mutfds,-1){Ok(polled_num)=>{ifpolled_num<=0{eprintln!("polled_num<=0!");break;}ifletSome(flag)=fds[0].revents(){ifflag.contains(PollFlags::POLLIN){letevents=read_fanotify(fanotify_fd);foreventinevents{handle_event(event)?;}}}ifletSome(flag)=fds[1].revents(){ifflag.contains(PollFlags::POLLIN){println!("receivedSIGTERMsignal");break;}}}Err(e)=>{ife==nix::Error::EINTR{continue;}eprintln!("Pollerror{:?}",e);break;}}}Ok(())}fnclose_fd(fd:i32){unsafe{libc::close(fd);}}fnhandle_event(event:FanotifyEvent)->io::Result<()>{letfd=event.fd;letevent_mask=event.mask;letkind=if(event_mask&FAN_ONDIR)!=0{"directory"}else{"file"};letpath=get_fd_path(fd)?;println!("{}{}mask{:b}.",kind,path.to_string_lossy(),event_mask);close_fd(fd);Ok(())}
  • 参数flags默认值 + FAN_EVENT_ON_CHILDpath为目录:

监控 path目录下所有文件的文件系统事件。

  • 参数flags默认值 + FAN_EVENT_ON_CHILDpath为容器 rootfs目录,无法监听到事件。(不能跨 mount namespace[7])

  • 参数flags默认值,传入目录但是没有 FAN_ONDIR:不能监听到事件。

  • 参数:flags默认值 + FAN_ONDIR

监听到 path目录本身的文件系统事件。

  • 参数:flagsFAN_MARK_MOUNTpath为目录:监听到 path所在挂载点的事件。

  • 参数:flagsFAN_MARK_MOUNTpath为挂载点下的文件:

mount--bindtestfatest

监听到挂载点下所有文件(包括子目录)的文件系统事件(dir1/file2.txt 是 path 子目录中的文件)。

  • 参数flagsFAN_MARK_MOUNTpath为容器 rootfs目录,无法监听到事件。(不能跨 mount namespace)

  • 参数:flagsFAN_MARK_FILESYSTEMpath为目录,监听到目录所在文件系统的事件。包括其它挂载点。

  • 参数:flagsFAN_MARK_FILESYSTEMpath为容器 rootfs

可以监听到 rootfs 下的文件系统事件,但看起来不完整,例如,下面的例子中,正确的结果应包括访问 /etc/hosts 的事件。

因此,使用 fanotify监听容器 rootfs中文件系统的最终解决方案:进入容器所在的 mount namespace,使用 FAN_MARK_MOUNTflag监听容器的根目录,即可递归监听容器中所有文件的事件。

Setns

基本介绍

setns[8] 是 Linux 的系统调用,允许进程切换到另一个进程所在的命名空间。Linux 中的命名空间提供了内核级别的资源隔离,不同命名空间中的程序享有独立的资源。目前,提供了 8 种资源隔离:

  • Mount: 文件系统挂载点,flag: CLONE_NEWNS(mount namespace 是最早提出的命名空间,所以 flag定为 CLONE_NEWNS,而不是CLONE_NEWMNT

  • UTS: 主机名和域名信息,flag: CLONE_NEWUTS

  • IPC: 进程间通信,flag: CLONE_NEWIPC

  • PID: 进程 ID,flag: CLONE_NEWPID

  • Network: 网络资源,flag: CLONE_NEWNET

  • User: 用户和用户组的 ID,flag: CLONE_NEWUSER

  • CGROUP:Cgroup 资源,flagCLONE_NEWCGROUP(从 Linux 4.6 开始支持)

  • Time:时间资源,flagCLONE_NEWTIME(从 Linux 5.8 开始支持)

Linux 中操作命名空间除了 setns,还有 cloneunshare系统调用。

  • setns:给已存在进程设置已存在的命名空间。

  • clone:创建新进程时,使用新的命名空间。(默认使用父进程的命名空间)

  • unshare:让已存在进程使用新的命名空间。

API 介绍

函数声明如下:

#define_GNU_SOURCE/*Seefeature_test_macros(7)*/#includeintsetns(intfd,intnstype);

fd 和已存在进程有关:

  • 已存在进程 /proc/[pid]/ns/ 目录下的不同命名空间对应文件的 fd:

nstype指定命名空间的类型,包括以下值:

  1. 0:任意类型(最好在知道 fd 指向命名空间类型时使用)

  2. CLONE_NEWCGROUP:fd 必须指向 cgroup 命名空间(从 Linux 4.6 开始支持),调用者需拥有 CAP_SYS_ADMIN能力,setns 不会修改原 cgroup 中子 cgroup 的命名空间。

  3. CLONE_NEWIPC:fd 必须指向 IPC 命名空间(从 Linux 3.0 开始支持),在原 user 命名空间和目标命名空间都需要有 CAP_SYS_ADMIN能力。

  4. CLONE_NEWNET:fd 必须指向 network 命名空间(从 Linux 3.0 开始支持),在原 user 命名空间和目标命名空间都需要有 CAP_SYS_ADMIN能力。

  5. CLONE_NEWNS:fd 必须指向 mount 命名空间(从 Linux 3.8 开始支持),在原命名空间需要有 CAP_SYS_CHROOTCAP_SYS_ADMIN能力,在目标命名空间都需要有 CAP_SYS_ADMIN能力。如果和其它进程(通过 cloneCLONE_FS实现)共享文件系统属性,则不能加入新的 mount 命名空间。

  6. CLONE_NEWPID:fd 必须指向 PID 命名空间(从 Linux 3.8 开始支持),在原 user 命名空间和目标命名空间都需要有 CAP_SYS_ADMIN能力。PID 和其它命名空间不同,sentns 加入 PID 命名空间之后,并不会修改 caller 的 PID 命名空间,加入之后,由 caller 创建的子进程使用新的 PID 命名空间。

  7. CLONE_NEWTIME:fd 必须指向 time 命名空间(从 Linux 5.8 开始支持),在原 user 命名空间和目标命名空间都需要有 CAP_SYS_ADMIN能力。

  8. CLONE_NEWUSER:fd 必须指向 user 命名空间(从 Linux 3.8 开始支持),必须在目标命名空间有 CAP_SYS_ADMIN能力。多线程程序加入 user 命名空间可能失败。不允许使用 setns 再次进入 caller 所在的 user 命名空间。出于安全原因考虑,如果和其它进程(通过 cloneCLONE_FS实现)共享文件系统属性,则不能加入新的 user 命名空间。

  9. CLONE_NEWUTS:fd 必须指向 UTS 命名空间(从 Linux 3.0 开始支持),在原 user 命名空间和目标命名空间都需要有 CAP_SYS_ADMIN能力。

  • 进程 PID 的文件描述符(详见 pidfd_open[9],Linux 5.8 开始支持)

nstype指定要加入的命名空间类型。例如:要加入 PID 为 1234 所在的 USER、NET、UTS 命名空间,保持其它命名空间不变:

intfd=pidfd_open(1234,0);setns(fd,CLONE_NEWUSER|CLONE_NEWNET|CLONE_NEWUTS);

实例

usenix::{sched::{setns,CloneFlags},};usestd::{env,fs,io,os::fd::AsRawFd,path::Path,process::Command};#[derive(Debug)]enumSetnsError{IO(io::Error),Nix(nix::Error),}fnset_ns(ns_path:String,flags:CloneFlags)->Result<(),SetnsError>{letfile=fs::File::open(Path::new(ns_path.as_str())).map_err(SetnsError::IO)?;setns(file.as_raw_fd(),flags).map_err(SetnsError::Nix)}fnjoin_namespace(pid:String)->Result<(),SetnsError>{set_ns(format!("/proc/{pid}/ns/pid"),CloneFlags::CLONE_NEWPID)?;set_ns(format!("/proc/{pid}/ns/ipc"),CloneFlags::CLONE_NEWIPC)?;set_ns(format!("/proc/{pid}/ns/cgroup"),CloneFlags::CLONE_NEWCGROUP,)?;set_ns(format!("/proc/{pid}/ns/net"),CloneFlags::CLONE_NEWNET)?;set_ns(format!("/proc/{pid}/ns/mnt"),CloneFlags::CLONE_NEWNS)?;Ok(())}fnprint_ns(path:&str){letoutput=Command::new("/bin/ls").arg("-l").arg(path).output().expect("failedtoexecuteprocess");ifoutput.status.success(){println!("{}",String::from_utf8_lossy(&output.stdout));}else{println!("err:{}",String::from_utf8_lossy(&output.stderr));}}fnmain(){letargs:Vec=env::args().collect();ifargs.len()<2{eprintln!("Usage:{}",args[0]);std::process::exit(1);}print_ns("/proc/self/ns");ifletErr(e)=join_namespace(args[1].clone()){eprintln!("joinnamespacefailed{e:?}");return;}print_ns("/proc/self/ns");}

可以看到,进程的 PID、IPC、Cgroup、NET、MNT 命名空间已经设置为容器的命名空间。

部分细节:

  1. 如需加入多个命名空间,MNT 命名空间应该最后加入。(先加入 MNT 命名空间会导致接下来的 join 操作无法正确读取原 /proc/pid/ns下的文件)

  2. 在本文使用的测试环境,加入 USER 命名空间会出错,返回 EINVAL 信息。

Nsenter

nsenter是 linux 中的命令行工具,位于 util-linux[10] 包中,用于进入目标进程的命名空间运行程序。

❯nsenter--helpUsage:nsenter[options][[...]]Runaprogramwithnamespacesofotherprocesses.Options:-a,--allenterallnamespaces-t,--targettargetprocesstogetnamespacesfrom-m,--mount[=]entermountnamespace-u,--uts[=]enterUTSnamespace(hostnameetc)-i,--ipc[=]enterSystemVIPCnamespace-n,--net[=]enternetworknamespace-p,--pid[=]enterpidnamespace-C,--cgroup[=]entercgroupnamespace-U,--user[=]enterusernamespace-T,--time[=]entertimenamespace-S,--setuidsetuidinenterednamespace-G,--setgidsetgidinenterednamespace--preserve-credentialsdonottouchuidsorgids-r,--root[=]settherootdirectory-w,--wd[=]settheworkingdirectory-W.--wdnssettheworkingdirectoryinnamespace-F,--no-forkdonotforkbeforeexec"ing

进入容器的 PID、Mount 命名空间:

nsenter-m-p-t113503bash

Fanotify 监控容器

  • 通过 setns 加入容器所在的 PID、Mount 命名空间;

  • 启动 fanotify server;

  • 监听到 fanotify 事件,通过 readlink 得到 fd 对应的 path;(由于已经处于容器所在的 PID 命名空间,因此,可以直接通过 /proc/self/fd/{fd}得到 fanotify 事件 fd 对应的 path)

  • Server 通过 stdout 将包含 path 的 fanotify 事件发送给 client;(由于已经处于容器所在 mount 命名空间,不能直接通过 socket 等进程间通信方式传输给 client)

  • client 对 fanotify 事件进行处理。

完整代码参考:optimizer-server[11]。

总结

  • Inotify 支持在节点上监听容器 rootfs 下的目录和文件。

  • Inotify 不支持递归监控子目录和文件。

  • Fanotify 监听 inode 对象和挂载点不支持跨 mount namespace,因此不支持在节点上直接监听容器 rootfs 下的目录和文件。

  • Fanotify(除 directed模式)支持递归监控目录下的子目录和文件。

  • Fanotify 可以借助 setns进入容器所在的 mount 命名空间,通过 per-mount模式实现递归监听容器 rootfs下的事件。

参考资料

[1]inode: https://en.wikipedia.org/wiki/Inode

[2]John McCutchan: http://johnmccutchan.com/

[3]inotify.7: https://man7.org/linux/man-pages/man7/inotify.7.html

[4]Fanotify: https://man7.org/linux/man-pages/man7/fanotify.7.html

[5]linux/fsnotify_backend.h#L185: https://github.com/torvalds/linux/blob/31a371e419c885e0f137ce70395356ba8639dc52/include/linux/fsnotify_backend.h#L185

[6]fanotify_init.2: https://man7.org/linux/man-pages/man2/fanotify_init.2.html

[7]mount namespace: https://man7.org/linux/man-pages/man7/mount_namespaces.7.html

[8]setns: https://man7.org/linux/man-pages/man2/setns.2.html

[9]pidfd_open: https://man7.org/linux/man-pages/man2/pidfd_open.2.html

[10]util-linux: https://github.com/util-linux/util-linux/blob/master/sys-utils/nsenter.c

[11]optimizer-server: https://github.com/containerd/nydus-snapshotter/tree/main/tools/optimizer-server

关键词: