/proc/pid

每一位 Linux 开发者都应该了解过 proc 文件系统,它位于 /proc,通常的一个用处就是列出 pid:

caturra@DESKTOP-P4DDLG1:/proc$ ls -F
1/     181/  264/  641/  acpi/      devices      ioports      kpageflags  net@          sysvipc/
120/   192/  271/  648/  buddyinfo  diskstats    irq/         loadavg     pagetypeinfo  thread-self@
121/   193/  288/  663/  bus/       dma          kallsyms     locks       partitions    timer_list
122/   194/  4/    682/  cgroups    driver/      kcore        mdstat      schedstat     tty/
123/   201/  630/  7/    cmdline    execdomains  key-users    meminfo     self@         uptime
128/   202/  631/  724/  config.gz  filesystems  keys         misc        softirqs      version
1335/  203/  632/  781/  consoles   fs/          kmsg         modules     stat          vmallocinfo
1396/  209/  639/  8/    cpuinfo    interrupts   kpagecgroup  mounts@     swaps         vmstat
1577/  246/  640/  9/    crypto     iomem        kpagecount   mtrr        sys/          zoneinfo

其中,数字开头的是当前系统用到的 pid。这里使用 -F 可以区分出它们是一个目录,内部以文件形式展示对应 pid 的相关信息。

/proc/tid

需要注意的是,如果一个进程(线程组)有多个线程,那么 /proc 只展示不重复的 tgid。

虽然对 /proc 的读操作并不显示线程目录,但仍可通过 /proc/[tid] 的方式进行访问。继续上述示例:

# 上述ls示例中并没有"6/"
caturra@DESKTOP-P4DDLG1:/proc$ cd 6
# 但是可以定位
caturra@DESKTOP-P4DDLG1:/proc/6$

caturra@DESKTOP-P4DDLG1:/proc/6$ sudo ls
arch_status      cpuset   limits      net            root          stat            uid_map
attr             cwd      loginuid    ns             sched         statm           wchan
auxv             environ  map_files   oom_adj        schedstat     status
cgroup           exe      maps        oom_score      sessionid     syscall
clear_refs       fd       mem         oom_score_adj  setgroups     task
cmdline          fdinfo   mountinfo   pagemap        smaps         timens_offsets
comm             gid_map  mounts      personality    smaps_rollup  timers
coredump_filter  io       mountstats  projid_map     stack         timerslack_ns

/proc/pid/task/tid

每个进程(线程组)所持有的线程 ID 可以通过 /proc/[pid]/task/ 得到。

实际上,前面示例的数字 6init 产生的一个线程:

caturra@DESKTOP-P4DDLG1:/proc$ cd 1
caturra@DESKTOP-P4DDLG1:/proc/1$ cd task
caturra@DESKTOP-P4DDLG1:/proc/1/task$ ls -F
1/  6/

目录内容与上述 /proc/6 一致,这里就不列出了。


各种目录细节见 man 5 proc。

proc root

为什么 ls /proc 明明只输出 pid,/proc/[tid] 又能访问到?因为 /proc 作为伪文件系统(pseudo-filesystem),其输出内容可以是每次读操作时动态生成的。

通过 strace ls /proc 跟踪系统调用,节选关键部分:

# 打开/proc,fd对应3
openat(AT_FDCWD, "/proc", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
# 获取fd=3的信息
newfstatat(3, "", {st_mode=S_IFDIR|0555, st_size=0, ...}, AT_EMPTY_PATH) = 0
# 类似调用readdir(3),参考man 2 getdents:
#   These are not the interfaces you are interested in.  Look at
#   readdir(3) for the POSIX-conforming C library interface.
getdents64(3, 0x559cc061f4f0 /* 63 entries */, 32768) = 1832
getdents64(3, 0x559cc061f4f0 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(1, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}, AT_EMPTY_PATH) = 0
write(1, "1    8\t\tcgroups    crypto     ex"..., 1161 # 后面都是标准输出,略

其中 getdents64() 内部途经 iterate_dir() 并调用 VFS 接口 file->f_op->iterate_shared

int iterate_dir(struct file *file, struct dir_context *ctx)
{
        struct inode *inode = file_inode(file);
        int res = -ENOTDIR;

        if (!file->f_op->iterate_shared)
                goto out;

        // ...

        if (!IS_DEADDIR(inode)) {
                ctx->pos = file->f_pos;
                res = file->f_op->iterate_shared(file, ctx);
        }

        // ...
}

iterate_shared 接口参考内核文档说明:called when the VFS needs to read the directory contents when filesystem supports concurrent dir iterators.

通过接口找实现,proc 的根目录对应 fop 如下:

/*
 * The root /proc directory is special, as it has the
 * <pid> directories. Thus we don't use the generic
 * directory handling functions for that..
 */
static const struct file_operations proc_root_operations = {
        .read            = generic_read_dir,
        .iterate_shared  = proc_root_readdir,
        .llseek         = generic_file_llseek,
};

static int proc_root_readdir(struct file *file, struct dir_context *ctx)
{
        // pos 为 0 表示.,1 表示..,[2, FIRST_PROCESS_ENTRY) 表示一些固定的文件
        // 通过同一个 ctx 贯穿重复整个过程
        if (ctx->pos < FIRST_PROCESS_ENTRY) {
                int error = proc_readdir(file, ctx);
                if (unlikely(error <= 0))
                        return error;
                ctx->pos = FIRST_PROCESS_ENTRY;
        }

        // 关键所在
        return proc_pid_readdir(file, ctx);
}

/* for the /proc/ directory itself, after non-process stuff has been done */
int proc_pid_readdir(struct file *file, struct dir_context *ctx)
{
        struct tgid_iter iter;
        struct proc_fs_info *fs_info = proc_sb_info(file_inode(file)->i_sb);
        struct pid_namespace *ns = proc_pid_ns(file_inode(file)->i_sb);
        loff_t pos = ctx->pos;

        // ...

        iter.tgid = pos - TGID_OFFSET;
        iter.task = NULL;
        // 关键遍历
        for (iter = next_tgid(ns, iter);
             iter.task;
             iter.tgid += 1, iter = next_tgid(ns, iter)) {
                char name[10 + 1];
                unsigned int len;

                cond_resched();
                if (!has_pid_permissions(fs_info, iter.task, HIDEPID_INVISIBLE))
                        continue;

                // 关键操作
                len = snprintf(name, sizeof(name), "%u", iter.tgid);
                ctx->pos = iter.tgid + TGID_OFFSET;
                if (!proc_fill_cache(file, ctx, name, len,
                                     proc_pid_instantiate, iter.task, NULL)) {
                        put_task_struct(iter.task);
                        return 0;
                }
        }
        ctx->pos = PID_MAX_LIMIT + TGID_OFFSET;
        return 0;
}

其核心函数就是遍历 tgid 的循环,通过复杂的 proc_fill_cache() 建立 dentry。

因此结论是 ls /proc 只显示 tgid。

proc lookup

访问 /proc/[tgid or tid] 需要 VFS 调用 proc root 的 lookup 接口。

/*
 * proc root can do almost nothing..
 */
static const struct inode_operations proc_root_inode_operations = {
        .lookup	        = proc_root_lookup,
        .getattr        = proc_root_getattr,
};

static struct dentry *proc_root_lookup(struct inode * dir, struct dentry * dentry, unsigned int flags)
{
        if (!proc_pid_lookup(dentry, flags))
                return NULL;

        return proc_lookup(dir, dentry, flags);
}

struct dentry *proc_pid_lookup(struct dentry *dentry, unsigned int flags)
{
        struct task_struct *task;
        unsigned tgid;
        struct proc_fs_info *fs_info;
        struct pid_namespace *ns;
        struct dentry *result = ERR_PTR(-ENOENT);

        // tgid 命名不对,只是一个 dentry 分量名字
        tgid = name_to_int(&dentry->d_name);
        if (tgid == ~0U)
                goto out;

        fs_info = proc_sb_info(dentry->d_sb);
        ns = fs_info->pid_ns;
        rcu_read_lock();
        // 通过分量转型后的整数来寻找 task
        task = find_task_by_pid_ns(tgid, ns);
        if (task)
                get_task_struct(task);
        rcu_read_unlock();
        if (!task)
                goto out;

        /* Limit procfs to only ptraceable tasks */
        if (fs_info->hide_pid == HIDEPID_NOT_PTRACEABLE) {
                if (!has_pid_permissions(fs_info, task, HIDEPID_NO_ACCESS))
                        goto out_put_task;
        }

        result = proc_pid_instantiate(dentry, task, NULL);
out_put_task:
        put_task_struct(task);
out:
        return result;
}

/*
 * Must be called under rcu_read_lock().
 */
struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns)
{
        RCU_LOCKDEP_WARN(!rcu_read_lock_held(),
                         "find_task_by_pid_ns() needs rcu_read_lock() protection");
        // 关键:PIDTYPE_PID 而不是 PIDTYPE_TGID
        return pid_task(find_pid_ns(nr, ns), PIDTYPE_PID);
}

这里面的 ID 类型 是有所区别的:

enum pid_type
{
        PIDTYPE_PID,
        PIDTYPE_TGID,
        PIDTYPE_PGID,
        PIDTYPE_SID,
        PIDTYPE_MAX,
};

从内核视角来看,线程与进程同样作为 task_struct,均具有唯一的 pid(不同于用户视角)。

因此结论是访问 /proc/[tid] 实际为查询 [kernel_pid] 的意思。

附:glibc 的「坑」

glibc 提供系统调用的封装 getpid()gettid(),这其实在命名上非常令人混淆。getpid() 实际返回内核视角的 tgid,而 gettid() 实际返回内核视角的 pid。从上面的 pid_type 也可以知道,内核根本不关心所谓的 tid。

getpid() returns the process ID (PID) of the calling process…From a kernel perspective, the PID (which is shared by all of the threads in a multithreaded process) is sometimes also known as the thread group ID (TGID).

References

Linux source code (v6.4.8) – Bootlin
proc(5) – Linux manual page
getpid(2) – Linux manual page
getdents(2) – Linux manual page
Overview of the Linux Virtual File System – The Linux Kernel Archives