关注了就能看到更多这么棒的文章哦~
process_madvise(), pidfd capabilities, and the revenge of the PIDs
By Jonathan Corbet
January 21, 2020
原文来自:https://lwn.net/Articles/810076/
从前,在某个进程被创建之后,其他进程就很难有办法对它进行操作了,只有给它发signal或者用ptrace()。现在,人们越来越有需求要能控制其他进程了,相应地,kernel的进程管理API也得到了扩展。process_madvise()系统调用就是其中一种,用来控制另一个进程内部内存管理方式的。这个最近提出来的一个process_ madvise(),尽管它有其合理性,但是也引起了许多人的提问,都是有关今后进程管理方式该如何改进的。
现有的madvise()系统调用是供某个进程来告诉kernel,关于他自己的地址空间该如何管理的。5.4的内核的madvise()调用中就多了一些新的建议类型:MADV_COLD和MADV_PAGEOUT。前者要求kernel把这块区域的内存页放到inactive list上去,也就是说它们已经挺长时间没有被访问过了。这些页面在后续内核进行内存回收的时候就会优先回收。MADV_PAGEOUT则语气更强烈一些,直接就说这些页面不会再用了,因此会导致马上被回收。
对于那些非常了解自己未来的访问模式的进程来说,这两个新类型会很有用处。不过在某些场景下——例如Android——进程其实并不清楚这个信息。在某些内存不再需要使用的情况,只有系统中的管理进程知道。比如说,在某个前台进程被放到后台的时候,这个进程的许多地址空间就可以标记成MADV_COLD了。这种环境中,要想最大限度地利用系统的内存资源,就需要让某个进程来替其他进程调用madvise()。这就是为什么要有process_madvise()的原因了。
Minchan Kim在1月初的时候就提过一组process_madvise() patch,当时的API是这样的:
int process_madvise(int pidfd, void *addr, size_t length, int advice,
unsigned long flags);
效果上就像是pidfd所代表的这个进程自己调用了madvise()一样,参数也是addr, length, 和advice。这里的flags参数其实没有用,并且只支持madvise()的MADV_系列参数的一个子集(MADV_COLD,MADV_PAGEOUT,MADV_MERGEABLE,MADV_UNMERGEABLE)。其他的参数计划等到确有需要的时候再加。
从目前为止关于process_madvise()的讨论来看,基本没有什么阻碍,很快能进入mainline了。不过接下来,就有一些问题跳了出来。
Pidfd capabilities
其中一个问题跟process_madvise()并不直接相关,而是跟pidfd API通常用法有关。目前的patch set里面,是否能允许一个进程调用process_madvise()来操作另一个进程,是通过问一个问题来确定的:“是否允许用ptrace()?”。所以,这就意味着要么是同一个user ID的两个进程,要么就是拥有CAP_SYS_PTRACE的权限。这个判断问题本身并没有什么争议,不过Christian Brauner(pidfd patch的主要作者)提出了另一个想法:
When you create a pidfd, e.g. with clone3() and you'd wanted to use it for madvise you'd need to set a flag like pidfd_cap_madvise or pidfd_feature_madvise when you create the pidfd. Only if the pidfd was created with that flag set could you use it with madvise.
简单来说,pidfd自己包含了一个capability mask,来指定可以对它进行什么操作。这就像是通常的文件描述符一样:人们打开文件的时候如果允许了写操作,那么就可以对这个文件描述符进行写操作。Brauner希望在5.7的时候再合入process_madvise(),这样它就能直接开始使用这个capabilty机制了(目前还没实现好)。
关于pidfd的capability工作机制,Daniel Colascione问了个有趣的问题:是不是pidfd带有相应的capability flag就可以进行这个操作了,还是此前的传统判断方式也可以继续用?也就是说,加入如果一个进程拿到了另一个进程的pidfd,并且pidfd已经允许它传递给process_madvise()了,那么这个进程是否还需要试着进行ptrace()来判断是否可以操作?
Brauner在这个问题上有过反复。他说他倾向仍然需要检查权限,不完全依赖pidfd的capability flag。这样这些capability flag就变成了一个关闭开关,用来限制哪些操作是被禁止的。Colascione更希望直接用pidfd的capability flag来判断权限就够了。Brauner答应会仔细考虑这个方案,近期会发出patch。
PIDs: not dead yet
这个process_madvise() API的另一个特点是它必须要使用pidfd,这一点跟其他使用process ID的kill()或setpriority()等系统调用不同。pidfd是这个领域的新宠,通常来说大家都倾向于用pidfd。pid在哥哥namespace里面可能会不同,并且在目标进程退出之后,有一定的概率被用在另一个完全不相关的进程上;而pidfd则可以确定指向某个进程,不会变动。因此,pidfd很快变成了所有新增的系统调用中最受欢迎的确认进程的方式。Colascione的说法是:“All new APIs should use pidfds: they're better than numeric PIDs in every way”。
不过,看来并不是所有人都赞同这个观点。很多场景下pid本来就是已知的,而pidfd还需要专门去创建出来。用户还可以在命令行上指定某个pid,而pidfd则没法这么用。Kirill Tkhai就是其中一个反对者:
Ordinary pid interfaces also should be available. There are a lot of cases, when they are more comfortable. Say, a calling of process_madvise() from tracer, when a tracee is stopped. In this moment the tracer knows everything about tracee state, and pidfd brackets pidfd_open() and close() around actual action look just stupid, and this is cpu time wasting.
因此他认为每个新增的进城相关API都应该要既能处理PID,又能处理pidfd。Kim很认真地吸取了这个提议,在一周后的第二版process_madvise() patch set中就实现好了。这样新增的API就变为了:
int process_madvise(int which, pid_t pid, void *addr, size_t length,
int advice, unsigned long flag);
这里新加的which参数要么是P_PID,要么是P_PIDFD,用来告诉kernel该如何理解pid参数。
这一改动提醒所有人都要注意一个问题。如果大家达成一致,都认为新增的进程相关API都应该同时支持这两种进程表达方式,那么就应该把这个规则推广开来,对新增的API一视同仁。否则process_madvise()也就不应该被强制要求这么做。如果今后某些API只支持一种方式,另一些API则两种都支持,那么就会是最坏的情况了。Linux的系统调用API目前已经有这样那样的一致性问题了,如果我们继续新增一些API让这个情况变得更差,未来的开发者会怎么看待我们?
全文完
LWN文章遵循CC BY-SA 4.0许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注LWN深度文章以及开源社区的各种新近言论~