Reputation: 2234
I need to check if a file is opened by any other process on the system. Scanning /proc
is not an option due to performance considerations.
An elegant way would be reading i_count
member of kernel's struct inode
, corresponding to the file.
I have an fd for the file in user space, how could I get i_count
of associated struct inode
from the kernel space, any idea?
Upvotes: 1
Views: 4562
Reputation: 1336
When an Open() system call is called on a kernel object (device file) , kernel creates an open file descriptor structure : struct file *
Always, a new struct file * is allocated in kernel every time an open() sys call is issued on a device file.
Objective is, for each open(), there should one and only one release method invocation in driver.
If a process has issues n open() sys calls on a device file/kernel object, n struct file structures are created in kernel.
Now, if a process is fork()/dup(), no new struct file are created in kernel. Only the reference count of existing struct file structures are incremented. This reference count denotes the number of processes sharing the file descriptor.
When close() is issued, reference count of struct file is decremented. If it reaches 0, then release method is invoked in driver.
This is how it is ensured that for each open(), there is only one and one release() invocation.
So, in a way, its not inode->icount, but filp->f_count that represents the no of processes sharing the same file descriptor.
Upvotes: 0
Reputation: 39426
This is not an answer to the stated question, but an example program how one can use Linux-specific file leases to reliably detect when access to a local file is exclusive on the current machine; that is, that the file is not open by any other process on the same machine. When a write lease is granted, the original process is notified when any other process tries to open the write-leased file.
writelease.c:
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#define LEASE_SIGNAL (SIGRTMIN+0)
int main(int argc, char *argv[])
{
sigset_t signals;
siginfo_t info;
int fd, result;
sigemptyset(&signals);
sigaddset(&signals, LEASE_SIGNAL);
sigaddset(&signals, SIGINT);
sigaddset(&signals, SIGTERM);
if (sigprocmask(SIG_BLOCK, &signals, NULL) == -1) {
fprintf(stderr, "Cannot block signals: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s FILENAME\n", argv[0]);
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
/* Open file. Should not be interrupted, but let's be overly paranoid. */
do {
fd = open(argv[1], O_RDONLY | O_NOCTTY);
} while (fd == -1 && errno == EINTR);
if (fd == -1) {
fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
/* This should not be interrupted ever, but let's again be overly paranoid. */
do {
result = fcntl(fd, F_SETSIG, LEASE_SIGNAL);
} while (result == -1 && errno == EINTR);
if (result == -1) {
fprintf(stderr, "%s: Cannot change signal: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
/* Try to get a write lease on the file. */
do {
result = fcntl(fd, F_SETLEASE, F_WRLCK);
} while (result == -1 && errno == EINTR);
if (result == -1) {
fprintf(stderr, "%s: Cannot get a write lease: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
printf("%s: Write lease obtained; this process (%ld) is the only one with an open description to it.\n",
argv[1], (long)getpid());
fflush(stdout);
/* Wait for the first signal. */
do {
info.si_fd = -1;
result = sigwaitinfo(&signals, &info);
} while (result == -1 && errno == EINTR);
if (result == -1) {
fprintf(stderr, "Uh-oh. sigwaitinfo() failed; this should never occur. %s.\n", strerror(result));
return EXIT_FAILURE;
}
if (result == LEASE_SIGNAL && info.si_fd == fd)
printf("Process %ld is opening the file. Releasing the lease.\n", (long)info.si_pid);
else
printf("Received signal %d (%s); exiting.\n", result, strsignal(result));
fflush(stdout);
/* Closing the file descriptor releases the lease.
* At exit, the kernel will do this for us, so explicit close() here
* is not necessary. Again, just being overly pedantic and careful. */
close(fd);
return EXIT_SUCCESS;
}
Compile the above using
gcc -std=c99 -Wall -Wextra -O2 writelease.c -o writelease
Normal users can only take leases on files they own. However, full superuser privileges are not needed to take leases on arbitrary files; the cap_lease
capability suffices. On most current Linux distributions, you can use
sudo setcap cap_lease=pe writelease
to add the capability to the binary; however, it means that any user can run it, and take a write lease on any file they wish. (Unless you vet the program first, to ensure that poses no security risks, you should not do that! However, it is very nice for testing on your own system.)
In one terminal window, take a lease on some file, perhaps on the writelease.c
file:
./writelease writelease.c
If the file is not open by any process, it will output something like
writelease.c: Write lease obtained; this process (5782) is the only one with an open description to it.
Note that many editors (like gedit
for example) do not keep the file permanently open, and may simply replace the old file with a new one using rename()
(or hard links). That is not caught by the file lease; you'd need to use fanotify or dnotify to detect those.
If the file is already open (say, less writelease.c
open in another window), the output will be different, most likely
writelease.c: Cannot get a write lease: Resource temporarily unavailable.
If the lease succeeded, you can interrupt the program using Ctrl+C (sending it an INT
signal), sending it a TERM
signal, or by using another program to open the file. For example, if you started less writelease.c
in another window, the write lease program would output
Process 1089 is opening the file. Releasing the lease.
and exit.
Limitations:
As noted above, this only reliably detects if the file is opened or truncated by another process. If the file or any of its parent directories are renamed, this will not catch that. You'd need to add fanotify or dnotify watches to the directory (and parent directories up to the mount point) to catch those. Of course, those occur only after the fact, not synchronously like file lease signals. For log rotation, that should not be a problem, however.
Only local files can be leased. For log files, this should not be a problem, since they should definitely be local. (For remote logging, you should redirect the entire syslog, and not use a network share for log files.)
When another process tries to open the file while you have a write lease on it, the open cannot be blocked indefinitely. (You can, of course, detect the process which tries to open it, and send it SIGKILL
to kill it, but that would be exceedingly brutal. In fact, I haven't even tried that myself, so I am not sure whether the lease is broken anyway after the grace period, /proc/sys/fs/lease-break-time
seconds, or not.) (I personally have truncated the file to zero bytes before relinguishing the lease, however.)
Even if the lease owner renames the file, or moves it into another directory on the same mount, the opener will still open the (renamed/moved) file. In a very fundamental way, the open is already in progress when the lease signal is sent, and we can only delay it for a handful of seconds, not really cancel it.
Upvotes: 2
Reputation:
I suggest you describe the actual problem.
In general, since the file can be opened at any time, the result of your check is immediately stale. Also i_count can be bumped despite no userspace users.
Upvotes: 1