问题描述
最近在 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 之间的描述符。
为什么会有这样的行为?
这里存在两个问题:
- lsof 为什么要关闭 3 到 RLIMIT_NOFILE 之间的文件描述符
- 为什么不从 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 函数,这只是平台差异中的一个函数。
除了这些内容之外,更多的是对参数的处理与对获取的数据的过滤、筛选,格式化输出,这些过程相当细节且复杂,这里就不进行描述了。