Một supervisor đọc PID 18427, kiểm tra đúng command line rồi chuẩn bị gửi SIGTERM. Trong khoảng giữa hai thao tác, process cũ thoát và kernel cấp lại PID 18427 cho process khác. Lệnh kill(18427, SIGTERM) bây giờ nhắm sai đối tượng. Đây là race kinh điển khi dùng PID dạng số như một “định danh ổn định”.
Linux pidfd giải quyết vấn đề bằng một file descriptor tham chiếu tới đúng task. Có thể theo dõi nó bằng poll/epoll, gửi signal bằng pidfd_send_signal() và chờ child bằng waitid(P_PIDFD) mà không phải định danh lại qua PID đã có thể tái sử dụng. Bài viết được đối chiếu với tài liệu Linux man-pages về pidfd_open(2), pidfd_send_signal(2), waitid(2) và procfs.
1. PID chỉ là tên có thể tái sử dụng
PID tồn tại trong một PID namespace và được kernel tái sử dụng sau khi process kết thúc. Chuỗi thao tác dưới đây có time-of-check/time-of-use race:
read_pid_from_file();
verify_proc_cmdline(pid);
kill(pid, SIGTERM);
Giữa verify và kill, target có thể thoát. Process mới nhận cùng PID sẽ bị signal nếu permission cho phép. Race tương tự xuất hiện khi:
- polling
/proc/<pid>để chờ process thoát; - đọc status rồi thao tác namespace;
- gắn timeout vào worker bằng PID;
- theo dõi hàng nghìn child với signal handler;
- truyền PID qua nhiều component có độ trễ.
Mở directory /proc/<pid> không tương đương pidfd đầy đủ: tài liệu kernel lưu ý file descriptor procfs không ngăn numeric PID được tái sử dụng, đồng thời nó không có đầy đủ semantics poll/wait của pidfd.
2. pidfd là gì?
PID file descriptor là file descriptor của kernel tham chiếu tới một task cụ thể. Khi đã lấy được pidfd, việc target thoát không làm descriptor “trỏ sang” process mới nhận cùng PID. Các thao tác dựa trên pidfd giữ đúng identity.
Các mốc API chính:
| Kernel | Khả năng |
|---|---|
| Linux 5.1 | pidfd_send_signal() |
| Linux 5.3 | pidfd_open() và pidfd pollable |
| Linux 5.4 | waitid(P_PIDFD, ...) |
| Linux 5.10 | PIDFD_NONBLOCK |
| Linux 6.9 | PIDFD_THREAD và signal scope cho thread/group |
Ứng dụng phải kiểm tra kernel/runtime và xử lý ENOSYS/EINVAL. pidfd là Linux-specific, không phải API portable POSIX.
3. Lấy handle cho process đang tồn tại
#define _GNU_SOURCE
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
static int pidfd_open_sys(pid_t pid, unsigned int flags)
{
return syscall(SYS_pidfd_open, pid, flags);
}
int pidfd = pidfd_open_sys(target_pid, 0);
if (pidfd == -1) {
perror("pidfd_open");
/* ESRCH: không có process tương ứng tại thời điểm syscall */
}
pidfd_open() thực hiện lookup và lấy reference trong một system call, loại bỏ race sau thời điểm mở handle. Descriptor được tạo với close-on-exec.
Tuy nhiên pidfd không sửa được sai lầm xảy ra trước khi gọi. Nếu PID đến từ file cũ hoặc API không tin cậy và đã được tái sử dụng, pidfd_open() sẽ ổn định reference tới process hiện đang sở hữu PID đó — có thể không phải target mà bạn tưởng. Supervisor cần nguồn identity tin cậy hoặc kiểm tra UID, cgroup, executable/build metadata theo mô hình phù hợp.
4. Tạo child cùng pidfd: ưu tiên clone3(CLONE_PIDFD)
Với process do chính supervisor tạo, cách mạnh nhất là nhận pidfd atomically trong lúc tạo child bằng clone()/clone3() và CLONE_PIDFD. Như vậy không có khoảng trống giữa tạo process và mở handle.
Mẫu fork() rồi pidfd_open(child_pid) có thể dùng khi các điều kiện giữ zombie được đáp ứng: SIGCHLD không bị bỏ qua, không dùng SA_NOCLDWAIT và child chưa bị thread/signal handler khác reap. Nếu không kiểm soát được các điều kiện này, child có thể biến mất trước khi handle được lấy.
Quy tắc thiết kế: process bạn tạo → lấy pidfd lúc tạo; process đã tồn tại → dùng pidfd_open(); đừng kéo dài tuổi thọ của một PID số rồi kỳ vọng nó vẫn chỉ cùng process.
5. Theo dõi process exit bằng poll/epoll
Pidfd có thể đưa vào event loop như socket hoặc pipe:
#include <poll.h>
struct pollfd item = {
.fd = pidfd,
.events = POLLIN,
};
int rc = poll(&item, 1, timeout_ms);
if (rc > 0 && (item.revents & POLLIN)) {
/* target đã kết thúc và trở thành zombie */
}
Khi process-level pidfd không dùng PIDFD_THREAD, descriptor readable sau khi thread cuối cùng trong thread group thoát. Sau khi task được reap, poll/epoll còn có thể báo hangup. read(pidfd, ...) không trả event data; trong implementation hiện hành nó thất bại với EINVAL. Pidfd là handle để poll và truyền cho syscall, không phải stream để đọc.
Với hàng nghìn worker, thêm pidfd vào một epoll instance giúp supervisor xử lý lifecycle trong cùng event loop, không cần quét /proc hoặc map PID dễ race.
6. Gửi signal đúng process bằng pidfd_send_signal
static int pidfd_send_signal_sys(
int pidfd,
int sig,
siginfo_t *info,
unsigned int flags
) {
return syscall(
SYS_pidfd_send_signal,
pidfd,
sig,
info,
flags
);
}
if (pidfd_send_signal_sys(pidfd, SIGTERM, NULL, 0) == -1) {
if (errno == ESRCH) {
/* task đã kết thúc/reap; không signal nhầm process mới */
} else {
perror("pidfd_send_signal");
}
}
Pidfd giữ identity nhưng không bỏ qua kiểm tra quyền. Caller vẫn cần quyền signal theo credential/namespace/LSM. Caller phải ở cùng PID namespace hoặc ancestor phù hợp theo contract của syscall.
Mẫu shutdown an toàn:
- Gửi
SIGTERMqua pidfd. - Poll pidfd với graceful timeout.
- Nếu chưa thoát, gửi
SIGKILLqua cùng pidfd. - Wait/reap nếu target là child.
- Đóng pidfd và xóa khỏi epoll.
Hai lần signal dùng cùng stable reference; PID được tái sử dụng ở nơi khác không ảnh hưởng.
7. Chờ child bằng waitid(P_PIDFD)
Nếu pidfd tham chiếu tới child của caller, có thể lấy exit information:
#define _GNU_SOURCE
#include <sys/wait.h>
siginfo_t status = {0};
if (waitid(P_PIDFD, pidfd, &status, WEXITED) == -1) {
perror("waitid");
} else {
printf("code=%d status=%d\n", status.si_code, status.si_status);
}
waitid() chỉ reap child theo quan hệ cha-con; pidfd không biến process bất kỳ thành child của caller. Có thể dùng WNOWAIT để quan sát trạng thái mà chưa reap, nhưng phải có bước reap sau đó để tránh zombie.
Nếu pidfd được mở với PIDFD_NONBLOCK và child chưa kết thúc, waitid() có thể trả EAGAIN thay vì block. Chọn blocking/nonblocking phù hợp event loop.
8. Supervisor event loop mẫu
Mỗi worker record nên lưu pidfd làm identity chính, PID số chỉ phục vụ log/telemetry:
struct worker {
int pidfd; /* stable identity */
pid_t pid; /* diagnostics only */
enum worker_state state;
uint64_t generation;
};
Luồng:
- Spawn worker và nhận pidfd atomically.
- Đăng ký pidfd với epoll kèm pointer/generation của record.
- Khi readable, gọi
waitid(P_PIDFD, ...). - Ghi exit cause, restart budget và release resource.
- Đóng pidfd; nếu restart thì tạo record và pidfd mới.
Không tái sử dụng record chỉ vì PID số giống nhau. Generation riêng giúp telemetry phân biệt các incarnation của cùng logical worker.
9. PID namespace và container
Một task có thể có PID khác nhau khi nhìn từ host và container. Pidfd tham chiếu object kernel thay vì một con số phải diễn giải lại trong từng namespace. Điều này hữu ích cho runtime/supervisor, nhưng không bỏ qua rule namespace: thao tác signal vẫn kiểm tra quan hệ PID namespace và quyền.
File descriptor có thể được truyền qua UNIX domain socket bằng SCM_RIGHTS. Một component có thể tạo process rồi chuyển pidfd cho supervisor khác mà không gửi PID mơ hồ. Tuy nhiên bên nhận vẫn phải xác thực peer và protocol; nhận một pidfd không tin cậy không tự chứng minh process có vai trò mong đợi.
Để dừng toàn bộ workload container hoặc cây process, một pidfd process-level có thể chưa đủ. Child có thể fork/grandchild, daemonize hoặc đổi process group. Dùng cgroup/systemd scope/container runtime cho lifecycle của một nhóm; pidfd quản lý chính xác từng process.
10. Thread pidfd và signal scope
Từ Linux 6.9, PIDFD_THREAD cho phép pidfd_open() tham chiếu thread cụ thể thay vì thread-group leader. Các flag signal mới phân biệt thread, thread group và process group.
Đừng bật các flag này chỉ vì header build có định nghĩa. Runtime kernel cũ có thể trả EINVAL. Thư viện nên feature-detect, giữ fallback rõ ràng và test semantics đa luồng, đặc biệt khi thread đã thoát nhưng process còn sống.
11. Các API nâng cao dùng pidfd
Pidfd còn có thể được dùng với:
pidfd_getfd(): duplicate một file descriptor của target, chịu kiểm tra kiểu ptrace/LSM;setns(): tham gia namespace của process được tham chiếu khi có quyền;process_madvise(): cung cấp memory advice cho target;- một số thư viện event loop/service manager tích hợp trực tiếp pidfd.
Đây là API mạnh và vẫn chịu permission checks. Không biến pidfd thành “capability vượt quyền”; giới hạn quyền service, seccomp, LSM và audit vẫn cần thiết.
12. pidfd không tự xác thực process
Pidfd trả lời câu hỏi: “các thao tác sau có nhắm cùng task mà handle đã mở không?” Nó không trả lời: “task này có phải workload hợp lệ của tôi không?”
Với process ngoài supervisor, xác thực có thể kết hợp:
- nguồn PID tin cậy và freshness;
- UID/GID mong đợi;
- cgroup/unit/container identity;
- executable path, inode hoặc build identity;
- application-level handshake;
- peer credentials khi pidfd được truyền qua socket.
Thu thập metadata ngay sau khi mở pidfd và quyết định fail closed nếu identity không khớp. Tránh mô hình “đọc PID file bất kỳ rồi signal”.
13. Resource lifecycle và giới hạn file descriptor
Mỗi pidfd tiêu thụ một file descriptor và kernel resource. Supervisor lớn phải:
- đặt
RLIMIT_NOFILEphù hợp; - đóng pidfd sau khi hoàn tất lifecycle;
- xóa fd khỏi epoll trước/đồng thời cleanup an toàn;
- không leak descriptor qua exec;
- metric số pidfd đang mở và lỗi
EMFILE/ENFILE; - giới hạn tốc độ restart để tránh churn resource.
pidfd_open() đặt close-on-exec sẵn, nhưng descriptor duplicate hoặc nhận qua IPC vẫn cần được audit.
14. Fallback trên kernel cũ
Nếu cần hỗ trợ Linux trước pidfd, không có fallback nào hoàn toàn tương đương. Có thể giảm rủi ro bằng cách:
- để process cha trực tiếp dùng
waitpid()cho child của nó; - đưa workload vào cgroup/systemd unit và điều khiển qua manager;
- kiểm tra PID cùng start time/identity sát thời điểm thao tác;
- giảm khoảng thời gian giữa lookup và action;
- fail closed cho thao tác nguy hiểm nếu không thể chứng minh identity.
Kiểm tra /proc/<pid>/stat rồi gọi kill() vẫn có race giữa check và signal. Đừng quảng bá nó như bảo đảm tương đương pidfd.
15. Kiểm thử đúng race
Unit test thông thường khó tạo PID reuse. Integration test nên:
- Spawn target, lấy pidfd và xác nhận poll chưa readable.
- Cho target thoát, xác nhận pidfd readable.
- Tạo nhiều process để gây churn PID trong namespace test cô lập.
- Xác nhận signal qua pidfd cũ trả lỗi, không chạm process mới.
- Test graceful timeout → SIGKILL → wait/reap.
- Test worker exec sang binary khác nhưng handle vẫn theo đúng task.
- Test kernel thiếu syscall/flag và fallback/fail-closed.
- Test fd exhaustion và cleanup epoll.
Không giảm pid_max hoặc tạo PID churn trên production. Dùng VM/PID namespace/runner dành riêng và giới hạn resource.
16. Chẩn đoán lỗi thường gặp
| Lỗi | Ý nghĩa thường gặp | Hướng xử lý |
|---|---|---|
ESRCH từ pidfd_open | PID không tồn tại tại lúc syscall | Coi target đã biến mất; không retry mù với PID cũ. |
ESRCH khi signal | Task đã kết thúc/reap | Kết thúc lifecycle; không lookup lại PID để signal. |
EPERM | Credential, namespace hoặc LSM từ chối | Sửa permission model; pidfd không bypass quyền. |
EINVAL với flag | Kernel không hỗ trợ flag/semantics | Feature-detect và fallback có kiểm soát. |
ECHILD từ waitid | Target không phải child có thể wait | Dùng poll để quan sát; chỉ reap child. |
EMFILE/ENFILE | Cạn fd process/hệ thống | Kiểm tra leak, limits và restart storm. |
17. Checklist production
- Dùng pidfd làm identity vận hành; PID chỉ để log.
- Process mới: ưu tiên
clone3(CLONE_PIDFD). - Process có sẵn: gọi
pidfd_open()và xác thực identity. - Monitor exit bằng poll/epoll, không quét
/proc. - Signal bằng
pidfd_send_signal(). - Child process: lấy status và reap bằng
waitid(P_PIDFD). - Dùng cgroup/service manager khi cần quản lý cả process tree.
- Feature-detect kernel và định nghĩa rõ fallback/fail-closed.
- Giới hạn fd, đóng handle và theo dõi leak.
- Kiểm thử PID churn trong môi trường cô lập.
Kết luận
PID số thuận tiện để hiển thị, nhưng không phải handle ổn định cho thao tác bất đồng bộ. pidfd đưa lifecycle process vào mô hình file descriptor quen thuộc của Linux: handle không đổi identity, poll được, signal đúng target và tích hợp với event loop. Một supervisor hiện đại nên lấy pidfd càng sớm càng tốt, dùng nó xuyên suốt vòng đời và chỉ giữ PID để chẩn đoán. Điều đó loại bỏ cả một lớp race nguy hiểm mà việc “kiểm tra thêm một lần trước kill” không thể giải quyết triệt để.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.