有没有一种可移植的方法(POSIX),可以获取当前进程最高已分配文件描述符编号?
我知道在 AIX 上有一种好方法可以获取这个编号,但我正在寻找一种可移植的方法。
我之所以问这个问题是因为我想关闭所有打开的文件描述符。我的程序是一个以 root 用户身份运行的服务器,它会为非 root 用户 fork 并执行子程序。在子进程中保留特权文件描述符会存在安全问题。某些文件描述符可能由我无法控制的代码(C 库、第三方库等)打开,因此我也不能依靠 FD_CLOEXEC
。
有没有一种可移植的方法(POSIX),可以获取当前进程最高已分配文件描述符编号?
我知道在 AIX 上有一种好方法可以获取这个编号,但我正在寻找一种可移植的方法。
我之所以问这个问题是因为我想关闭所有打开的文件描述符。我的程序是一个以 root 用户身份运行的服务器,它会为非 root 用户 fork 并执行子程序。在子进程中保留特权文件描述符会存在安全问题。某些文件描述符可能由我无法控制的代码(C 库、第三方库等)打开,因此我也不能依靠 FD_CLOEXEC
。
尽管可移植,但仅关闭到 sysconf(_SC_OPEN_MAX)
的所有文件描述符并不可靠,因为在大多数系统上,此调用返回当前文件描述符软限制,该限制可能已降低到低于最高使用的文件描述符。另一个问题是,在许多系统上,sysconf(_SC_OPEN_MAX)
可能会返回 INT_MAX
,这可能导致此方法速度过慢而无法接受。不幸的是,没有一种可靠的、可移植的替代方案,不涉及遍历每个可能的非负整数文件描述符。
尽管不可移植,今天大多数常用操作系统都提供以下一种或多种解决方案来解决此问题:
一个库函数用于关闭所有打开的文件描述符>=fd或在范围内。对于关闭所有文件描述符的常见情况,这是最简单的解决方案,尽管它不能用于其他用途。要关闭除某个集合之外的所有文件描述符,可以使用dup2
将它们移动到前面的低端,并在必要时将它们移回来。
closefrom(fd)
(Linux with glibc 2.34+,Solaris 9+,FreeBSD 7.3+,NetBSD 3.0+,OpenBSD 3.5+)
fcntl(fd, F_CLOSEM, 0)
(AIX,IRIX,NetBSD)
close_range(lowfd, highfd, 0)
(Linux kernel 5.9+ with glibc 2.34+,FreeBSD 12.2+)
一个库函数提供进程当前正在使用的最大文件描述符。要关闭超过某个数字的所有文件描述符,可以关闭所有文件描述符,直到达到此最大值,或者在循环中不断获取并关闭最高的文件描述符,直到达到下限。哪种方法更有效取决于文件描述符密度。
在NetBSD中,fcntl(0, F_MAXFD)
可以获取最大文件描述符数量。
在HP-UX中,pstat_getproc(&ps, sizeof(struct pst_status), (size_t)0, (int)getpid())
返回有关进程的信息,包括当前开放的最高文件描述符ps.pst_highestfd
。
一个库函数可以列出进程目前使用的所有文件描述符。这种方法更加灵活,因为它允许关闭所有文件描述符、查找最高文件描述符或仅处理每个打开的文件描述符,甚至可能是另一个进程的文件描述符。 示例 (OpenSSH)
proc_pidinfo(getpid(), PROC_PIDLISTFDS, 0, fdinfo_buf, sz)
(macOS)当前分配给进程的文件描述符槽位数为当前正在使用的文件描述符号提供了上限。 示例 (Ruby)
/proc/
pid/status
或/proc/self/status
中的“FDSize:”行 (Linux)一个包含每个打开文件描述符的条目的目录。与 #3 类似,但它不是库函数。对于常见用途,这可能比其他方法更复杂,并且可能因 proc/fdescfs 未挂载、chroot 环境或没有可用于打开目录的文件描述符(进程或系统限制)等各种原因而失败。因此,通常会将此方法与后备机制相结合使用。示例(OpenSSH),另一个示例(glib)。
/proc/
pid/fd/
或 /proc/self/fd/
(Linux、Solaris、AIX、Cygwin、NetBSD)
(AIX 不支持 "self
")
/dev/fd/
(FreeBSD、macOS)
使用这种方法很难可靠地处理所有角落情况。例如,考虑所有文件描述符>=fd 都要关闭,但所有文件描述符<fd都在使用中,当前进程资源限制为fd,并且存在已使用的文件描述符>=fd 的情况。由于进程资源限制已达到,因此无法打开目录。如果使用从fd 到资源限制或 sysconf(_SC_OPEN_MAX)
的每个文件描述符关闭作为后备机制,则什么也不会关闭。
POSIX方法是:
int maxfd=sysconf(_SC_OPEN_MAX);
for(int fd=3; fd<maxfd; fd++)
close(fd);
(请注意这是从3开始关闭,以保持stdin / stdout / stderr打开)
如果文件描述符未打开,则close()会无害地返回EBADF。没有必要再浪费一次系统调用来检查。
某些Unix支持closefrom()。这可以避免调用大量close()函数取决于可能的最大文件描述符号码。虽然这是我所知道的最佳解决方案,但它完全不可移植。
我已经编写了处理所有平台特定功能的代码。所有函数都是异步信号安全的。希望人们会觉得这很有用。目前仅在OS X上进行过测试,欢迎改进/修复。
// Async-signal safe way to get the current process's hard file descriptor limit.
static int
getFileDescriptorLimit() {
long long sysconfResult = sysconf(_SC_OPEN_MAX);
struct rlimit rl;
long long rlimitResult;
if (getrlimit(RLIMIT_NOFILE, &rl) == -1) {
rlimitResult = 0;
} else {
rlimitResult = (long long) rl.rlim_max;
}
long result;
if (sysconfResult > rlimitResult) {
result = sysconfResult;
} else {
result = rlimitResult;
}
if (result < 0) {
// Both calls returned errors.
result = 9999;
} else if (result < 2) {
// The calls reported broken values.
result = 2;
}
return result;
}
// Async-signal safe function to get the highest file
// descriptor that the process is currently using.
// See also https://dev59.com/lXNA5IYBdhLWcg3wmfEa
static int
getHighestFileDescriptor() {
#if defined(F_MAXFD)
int ret;
do {
ret = fcntl(0, F_MAXFD);
} while (ret == -1 && errno == EINTR);
if (ret == -1) {
ret = getFileDescriptorLimit();
}
return ret;
#else
int p[2], ret, flags;
pid_t pid = -1;
int result = -1;
/* Since opendir() may not be async signal safe and thus may lock up
* or crash, we use it in a child process which we kill if we notice
* that things are going wrong.
*/
// Make a pipe.
p[0] = p[1] = -1;
do {
ret = pipe(p);
} while (ret == -1 && errno == EINTR);
if (ret == -1) {
goto done;
}
// Make the read side non-blocking.
do {
flags = fcntl(p[0], F_GETFL);
} while (flags == -1 && errno == EINTR);
if (flags == -1) {
goto done;
}
do {
fcntl(p[0], F_SETFL, flags | O_NONBLOCK);
} while (ret == -1 && errno == EINTR);
if (ret == -1) {
goto done;
}
do {
pid = fork();
} while (pid == -1 && errno == EINTR);
if (pid == 0) {
// Don't close p[0] here or it might affect the result.
resetSignalHandlersAndMask();
struct sigaction action;
action.sa_handler = _exit;
action.sa_flags = SA_RESTART;
sigemptyset(&action.sa_mask);
sigaction(SIGSEGV, &action, NULL);
sigaction(SIGPIPE, &action, NULL);
sigaction(SIGBUS, &action, NULL);
sigaction(SIGILL, &action, NULL);
sigaction(SIGFPE, &action, NULL);
sigaction(SIGABRT, &action, NULL);
DIR *dir = NULL;
#ifdef __APPLE__
/* /dev/fd can always be trusted on OS X. */
dir = opendir("/dev/fd");
#else
/* On FreeBSD and possibly other operating systems, /dev/fd only
* works if fdescfs is mounted. If it isn't mounted then /dev/fd
* still exists but always returns [0, 1, 2] and thus can't be
* trusted. If /dev and /dev/fd are on different filesystems
* then that probably means fdescfs is mounted.
*/
struct stat dirbuf1, dirbuf2;
if (stat("/dev", &dirbuf1) == -1
|| stat("/dev/fd", &dirbuf2) == -1) {
_exit(1);
}
if (dirbuf1.st_dev != dirbuf2.st_dev) {
dir = opendir("/dev/fd");
}
#endif
if (dir == NULL) {
dir = opendir("/proc/self/fd");
if (dir == NULL) {
_exit(1);
}
}
struct dirent *ent;
union {
int highest;
char data[sizeof(int)];
} u;
u.highest = -1;
while ((ent = readdir(dir)) != NULL) {
if (ent->d_name[0] != '.') {
int number = atoi(ent->d_name);
if (number > u.highest) {
u.highest = number;
}
}
}
if (u.highest != -1) {
ssize_t ret, written = 0;
do {
ret = write(p[1], u.data + written, sizeof(int) - written);
if (ret == -1) {
_exit(1);
}
written += ret;
} while (written < (ssize_t) sizeof(int));
}
closedir(dir);
_exit(0);
} else if (pid == -1) {
goto done;
} else {
do {
ret = close(p[1]);
} while (ret == -1 && errno == EINTR);
p[1] = -1;
union {
int highest;
char data[sizeof(int)];
} u;
ssize_t ret, bytesRead = 0;
struct pollfd pfd;
pfd.fd = p[0];
pfd.events = POLLIN;
do {
do {
// The child process must finish within 30 ms, otherwise
// we might as well query sysconf.
ret = poll(&pfd, 1, 30);
} while (ret == -1 && errno == EINTR);
if (ret <= 0) {
goto done;
}
do {
ret = read(p[0], u.data + bytesRead, sizeof(int) - bytesRead);
} while (ret == -1 && ret == EINTR);
if (ret == -1) {
if (errno != EAGAIN) {
goto done;
}
} else if (ret == 0) {
goto done;
} else {
bytesRead += ret;
}
} while (bytesRead < (ssize_t) sizeof(int));
result = u.highest;
goto done;
}
done:
if (p[0] != -1) {
do {
ret = close(p[0]);
} while (ret == -1 && errno == EINTR);
}
if (p[1] != -1) {
do {
close(p[1]);
} while (ret == -1 && errno == EINTR);
}
if (pid != -1) {
do {
ret = kill(pid, SIGKILL);
} while (ret == -1 && errno == EINTR);
do {
ret = waitpid(pid, NULL, 0);
} while (ret == -1 && errno == EINTR);
}
if (result == -1) {
result = getFileDescriptorLimit();
}
return result;
#endif
}
void
closeAllFileDescriptors(int lastToKeepOpen) {
#if defined(F_CLOSEM)
int ret;
do {
ret = fcntl(lastToKeepOpen + 1, F_CLOSEM);
} while (ret == -1 && errno == EINTR);
if (ret != -1) {
return;
}
#elif defined(HAS_CLOSEFROM)
closefrom(lastToKeepOpen + 1);
return;
#endif
for (int i = getHighestFileDescriptor(); i > lastToKeepOpen; i--) {
int ret;
do {
ret = close(i);
} while (ret == -1 && errno == EINTR);
}
}
posix_spawn
,并将POSIX_SPAWN_CLOEXEC_DEFAULT
设置为posix_spawnattr_setflags
。posix_spawn
调用中明确设置的文件描述符打开,并关闭其他调用。当你的程序刚开始运行并且没有打开任何东西时,例如像main()函数的开始。使用管道和fork立即启动一个执行服务器。这样它的内存和其他细节就是干净的,你可以直接给它要分叉和执行的内容。
#include <unistd.h>
#include <stdio.h>
#include <memory.h>
#include <stdlib.h>
struct PipeStreamHandles {
/** Write to this */
int output;
/** Read from this */
int input;
/** true if this process is the child after a fork */
bool isChild;
pid_t childProcessId;
};
PipeStreamHandles forkFullDuplex(){
int childInput[2];
int childOutput[2];
pipe(childInput);
pipe(childOutput);
pid_t pid = fork();
PipeStreamHandles streams;
if(pid == 0){
// child
close(childInput[1]);
close(childOutput[0]);
streams.output = childOutput[1];
streams.input = childInput[0];
streams.isChild = true;
streams.childProcessId = getpid();
} else {
close(childInput[0]);
close(childOutput[1]);
streams.output = childInput[1];
streams.input = childOutput[0];
streams.isChild = false;
streams.childProcessId = pid;
}
return streams;
}
struct ExecuteData {
char command[2048];
bool shouldExit;
};
ExecuteData getCommand() {
// maybe use json or semething to read what to execute
// environment if any and etc..
// you can read via stdin because of the dup setup we did
// in setupExecutor
ExecuteData data;
memset(&data, 0, sizeof(data));
data.shouldExit = fgets(data.command, 2047, stdin) == NULL;
return data;
}
void executorServer(){
while(true){
printf("executor server waiting for command\n");
// maybe use json or semething to read what to execute
// environment if any and etc..
ExecuteData command = getCommand();
// one way is for getCommand() to check if stdin is gone
// that way you can set shouldExit to true
if(command.shouldExit){
break;
}
printf("executor server doing command %s", command.command);
system(command.command);
// free command resources.
}
}
static PipeStreamHandles executorStreams;
void setupExecutor(){
PipeStreamHandles handles = forkFullDuplex();
if(handles.isChild){
// This simplifies so we can just use standard IO
dup2(handles.input, 0);
// we comment this out so we see output.
// dup2(handles.output, 1);
close(handles.input);
// we uncomment this one so we can see hello world
// if you want to capture the output you will want this.
//close(handles.output);
handles.input = 0;
handles.output = 1;
printf("started child\n");
executorServer();
printf("exiting executor\n");
exit(0);
}
executorStreams = handles;
}
/** Only has 0, 1, 2 file descriptiors open */
pid_t cleanForkAndExecute(const char *command) {
// You can do json and use a json parser might be better
// so you can pass other data like environment perhaps.
// and also be able to return details like new proccess id so you can
// wait if it's done and ask other relevant questions.
write(executorStreams.output, command, strlen(command));
write(executorStreams.output, "\n", 1);
}
int main () {
// needs to be done early so future fds do not get open
setupExecutor();
// run your program as usual.
cleanForkAndExecute("echo hello world");
sleep(3);
}
exec
系列函数自动关闭。 - R.. GitHub STOP HELPING ICE