lsof 原理及其运行时关闭多个描述符的行为

问题描述

最近在 strace lsof 的时候发现它会在初始化的过程中,会关闭从 3 到 1024 的文件描述符。这个时候这些描述符并没有人打开,因此 close 系统调用会返回 -EBADF 的错误值。

strace 的相关输出信息如下:

prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=1024, rlim_max=1024*1024}) = 0
close(3)                                = -1 EBADF (错误的文件描述符)
close(4)                                = -1 EBADF (错误的文件描述符)
close(5)                                = -1 EBADF (错误的文件描述符)
close(6)                                = -1 EBADF (错误的文件描述符)
close(7)                                = -1 EBADF (错误的文件描述符)
close(8)                                = -1 EBADF (错误的文件描述符```
.....................

从上述系统调用的记录可以看出,lsof 先调用了 prlimit64,指定了 RLIMIT_NOFILE 以获取程序能够打开的最大的描述符号。当前限制是 1024,获取到这个限制后,lsof 关闭从 3 到 1023 之间的描述符。

为什么会有这样的行为?

这里存在两个问题:

  1. lsof 为什么要关闭 3 到 RLIMIT_NOFILE 之间的文件描述符
  2. 为什么不从 0 开始关闭文件描述符?

不从 0 开始关闭这个问题倒好解释,0、1、2 分别对应 stdin、stdout、stderr 是程序执行默认打开的文件描述符,这些也是程序正常运行过程中会使用到的,不应该 close 它。

第一个问题则可以通过阅读 lsof 命令的源码来寻找答案。

lsof 命令中的相关代码

执行 sudo apt-get source lsof 下载 lsof 的源码,在 main 函数中找到了与上面这个行为相关的代码,摘录如下:

/*
 * Close enough file descriptors above 2 that library functions will have
 * open descriptors.
 *
 * Make sure stderr, stdout, and stdin are open descriptors.  Open /dev/null
 * for ones that aren't.  Be terse.
 *
 * Make sure umask allows lsof to define its own file permissions.
 */

	if ((MaxFd = (int) GET_MAX_FD()) < 53)
	    MaxFd = 53;

#if	defined(HAS_CLOSEFROM)
	(void) closefrom(3);
#else	/* !defined(HAS_CLOSEFROM) */
	for (i = 3; i < MaxFd; i++)
	    (void) close(i);
#endif	/* !defined(HAS_CLOSEFROM) */

代码的逻辑与我在上文中的描述内容大致相同,从注释来看,这里关闭描述符相当于某种回收的行为,以便后续调用库函数的时候有足够的描述符能够分配。

GET_MAX_FD 宏的定义如下:

./proto.h:145:#define	GET_MAX_FD	getdtablesize

它实际是调用库函数 getdtablesize 来获取系统能够分配的最大的描述符号,这个库函数底层原理是首先调用 getrlimit 来获取 RLIMIT_NOFILE 限额,如果获取失败则返回 OPEN_MAX 的值。

getrlimit、setrlimit、prlimit 资源限额相关系统调用

回答了上面两个问题后,我想在这里继续描述下 getrlimit 获取、配置资源限额的系统调用。

这几个函数的函数原型如下:

       int getrlimit(int resource, struct rlimit *rlim);
       int setrlimit(int resource, const struct rlimit *rlim);

       int prlimit(pid_t pid, int resource, const struct rlimit *new_limit,
                   struct rlimit *old_limit);

从功能上来说,getrlimit 用来获取资源限额,setrlimit 用来设置资源限额,prmilit 是 linux 独有的系统调用,能够同时用来获取与设置资源限额。

细心的读者可能会注意到 prlimit 中的 pid 参数,这与 getrlimit 及 setrlimit 函数不同。一般来说我们调用 getrlimit 与 setrlimit 设定的资源配额都是针对当前进程不需要指定任何的 pid

这里 prlimit 增加的这个参数带来的功能是我们能够获取、设定指定进程的资源配额,不过这里也有严格的权限限制,一般来说一个用户只能通过 prlimit 设定属于自己的进程与相同分组的进程。

上面这几个系统调用都使用了相同的 struct rlimit 结构体,此结构体的定义内容如下:

struct rlimit {
    rlim_t rlim_cur;  /* Soft limit */
    rlim_t rlim_max;  /* Hard limit (ceiling for rlim_cur) */
};

这里 rlim_cur 是软件限制,rlim_max 是硬件限制。在学习 bash 的内置命令 ulimit 的时候也有学习到相应的知识。

软件限制是内核对相应资源的强制限定,硬件限制作为软件限制的天花板。一个非特权的进程能够将其软件限额设定为 0 到硬件限额之间,不能超过硬件限额

一个特权进程,在 linux 中的那些初始化的用户命名空间中拥有 CAP_SYS_RESOURCE 属性的进程可以任意修改限制值

bash 中的 ulimit

学习过 linux 命令的朋友应该都使用过 ulimit,它其实是一个 bash 的内置命令。它也可以用来获取、设置一些资源的配额。它也是通过调用上文中描述的 prlimit 相关的系统调用来实现的。

为了验证这点,我开启两个终端,在一个终端中执行 strace -p 追踪另外一个终端,然后在被追踪的终端中输入 ulimit -a 命令,发现 ulimit 确实调用了 prlimit 来获取资源配额信息。

相关内容部分截取如下;

prlimit64(0, RLIMIT_CORE, NULL, {rlim_cur=0, rlim_max=RLIM64_INFINITY}) = 0
write(1, "core file size          (blocks,"..., 39) = 39
prlimit64(0, RLIMIT_DATA, NULL, {rlim_cur=RLIM64_INFINITY, rlim_max=RLIM64_INFINITY}) = 0
write(1, "data seg size           (kbytes,"..., 47) = 47
prlimit64(0, RLIMIT_NICE, NULL, {rlim_cur=0, rlim_max=0}) = 0
write(1, "scheduling priority             "..., 39) = 39
prlimit64(0, RLIMIT_FSIZE, NULL, {rlim_cur=RLIM64_INFINITY, rlim_max=RLIM64_INFINITY}) = 0
write(1, "file size               (blocks,"..., 47) = 47
prlimit64(0, RLIMIT_SIGPENDING, NULL, {rlim_cur=30119, rlim_max=30119}) = 0
write(1, "pending signals                 "..., 43) = 43
prlimit64(0, RLIMIT_MEMLOCK, NULL, {rlim_cur=65536*1024, rlim_max=65536*1024}) = 0
write(1, "max locked memory       (kbytes,"..., 43) = 43

可以看到,这里用 prlimit64 获取了多个资源的限制信息,并输出到终端中。

lsof 命令的功能

这里将问题扯回到 lsof 上。man ls 命令发现 lsof 命令的功能介绍相当简单,它被用来列出系统中已经打开的文件信息。

执行如下命令可以查看到我当前系统中所有打开的文件统计。

[longyu@debian-10:07:25:08] ~ $ lsof | wc
 134061 1442958 19827798

可以看到有 13.4 万左右已经打开的文件,这个数量还是挺大的。

lsof 命令的工作原理

lsof 命令基本功能是输出系统中已经打开文件的信息,在此基础上它增加了针对不同类别的过滤与筛选功能,例如使用 uid、pid、gid、TCP UDP 状态名称、文件类型、网络地址、路径名、正则表达式等等。

lsof 针对不同的平台有不同的实现,不同的平台在 lsof 中被称为 dialects,不同平台的代码放在源码目录 dialects 子目录中。

我下载的源码包中,dialects 子目录中内容如下:

[longyu@debian-10:10:28:51] dialects $ ls
aix  darwin  du  freebsd  hpux  linux  n+obsd  n+os  osr  sun

在执行 ./configure 的时候需要指定一个 dialects,这样会生成针对特定平台的 Makefile 文件

不同的平台中,获取进程占用 fd 信息的过程有所区别,dialects 将这些变化封装在配置过程中。

对于 linux 来说,lsof 获取进程占用的 fd 信息是通过遍历 /proc/[pid]/fd 目录来实现的,这个目录下的文件代表了进程打开的描述符的号码,其内容是指向真实文件的软链接。

一个具体的示例如下:

[longyu@debian-10:10:35:36] fd $ ls -lh 
总用量 0
lrwx------ 1 longyu longyu 64 9月  12 06:25 0 -> /dev/pts/1
lrwx------ 1 longyu longyu 64 9月  12 06:25 1 -> /dev/pts/1
lrwx------ 1 longyu longyu 64 9月  12 06:25 2 -> /dev/pts/1
lrwx------ 1 longyu longyu 64 9月  12 07:22 255 -> /dev/pts/1

可以看到这里 0、1、2、255 这几个描述符都被打开,并且都指向 /dev/pts/1 这个设备文件。

lsof 命令需要遍历每一个 /proc/[pids] 目录,读取 fd 子目录与相关目录中的文件,从这些文件中就能够获取到不同进程打开的文件信息。

strace lsof 的部分输出

下面的内容是我使用 strace lsof 得到的部分输出信息:

readlink("/proc/4777/fd/1", "/dev/pts/1", 4096) = 10
lstat("/proc/4777/fd/1", {st_mode=S_IFLNK|0700, st_size=64, ...}) = 0
stat("/proc/4777/fd/1", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}) = 0
openat(AT_FDCWD, "/proc/4777/fdinfo/1", O_RDONLY) = 7
fstat(7, {st_mode=S_IFREG|0400, st_size=0, ...}) = 0
read(7, "pos:\t0\nflags:\t0102002\nmnt_id:\t23"..., 1024) = 33
read(7, "", 1024)                       = 0
close(7)                                = 0
                      = 0

可以看到 lsof 访问了 4777 这个进程的 proc 目录,通过遍历其中的 fd 子目录,调用 readlink 系统调用来获取进程打开的不同文件描述符指向的文件路径。

同时 lsof 还访问了 fdinfo 这个目录,这个目录中存储了不同文件描述符的 pos、flags 与 mnt_id 信息。

lsof 代码中的处理

lsof 中通过 gather_proc_info 函数遍历 proc 目录中不同进程的子目录,获取相关的信息,当进程表有多个表项时,不同的表项会以 pid 为 key 进行排序。

相关的代码内容如下:

	do {

	/*
	 * Gather information about processes.
	 */
	    gather_proc_info();
	/*
	 * If the local process table has more than one entry, sort it by PID.
	 */
	    if (Nlproc > 1) {
		if (Nlproc > sp) {
		    len = (MALLOC_S)(Nlproc * sizeof(struct lproc *));
		    sp = Nlproc;
		    if (!slp)
			slp = (struct lproc **)malloc(len);
		    else
			slp = (struct lproc **)realloc((MALLOC_P *)slp, len);
		    if (!slp) {
			(void) fprintf(stderr,
			    "%s: no space for %d sort pointers\n", Pn, Nlproc);
			Exit(1);
		    }
		}
		for (i = 0; i < Nlproc; i++) {
		    slp[i] = &Lproc[i];
		}
		(void) qsort((QSORT_P *)slp, (size_t)Nlproc,
			     (size_t)sizeof(struct lproc *), comppid);
	    }

gather_proc_info 不同的平台有不同的实现,在源码路径中搜索,检索到如下相关内容:

./dialects/n+os/dproc.c:122: * gather_proc_info() -- gather process information
./dialects/n+os/dproc.c:126:gather_proc_info()
./dialects/freebsd/dproc.c:104: * gather_proc_info() -- gather process information
./dialects/freebsd/dproc.c:108:gather_proc_info()
./dialects/sun/dproc.c:196: * gather_proc_info() - gather process information
./dialects/sun/dproc.c:200:gather_proc_info()
./dialects/osr/dproc.c:110: * gather_proc_info() -- gather process information
./dialects/osr/dproc.c:114:gather_proc_info()
./dialects/hpux/kmem/dproc.c:144: * gather_proc_info() -- gather process information
./dialects/hpux/kmem/dproc.c:148:gather_proc_info()
./dialects/hpux/pstat/dproc.c:133: * gather_proc_info() -- gather process information
./dialects/hpux/pstat/dproc.c:137:gather_proc_info()
./dialects/darwin/kmem/dproc.c:149: * gather_proc_info() -- gather process information
./dialects/darwin/kmem/dproc.c:153:gather_proc_info()
./dialects/darwin/libproc/dproc.c:168: * gather_proc_info() -- gather process information
./dialects/darwin/libproc/dproc.c:172:gather_proc_info()
./dialects/linux/dproc.c:194: * gather_proc_info() -- gather process information
./dialects/linux/dproc.c:198:gather_proc_info()
./dialects/du/dproc.c:146: * gather_proc_info() -- gather process information
./dialects/du/dproc.c:150:gather_proc_info()
./dialects/aix/dproc.c:332: * gather_proc_info() - gather process information
./dialects/aix/dproc.c:336:gather_proc_info()
./dialects/n+obsd/dproc.c:146: * gather_proc_info() -- gather process information
./dialects/n+obsd/dproc.c:150:gather_proc_info()

从上面的信息可以看到,每一个 dialects 中都单独实现了自己的 gather_proc_info 函数,这只是平台差异中的一个函数。

除了这些内容之外,更多的是对参数的处理与对获取的数据的过滤、筛选,格式化输出,这些过程相当细节且复杂,这里就不进行描述了。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值