如何正确地获取线程ID? / 2022-01-09

如何正确的获取线程ID?这个问题看似简单实则暗藏陷阱。由于存在用户态和内核态两层线程模型,就有两种获取线程ID的方式。

首先要搞明白什么是POSIX。很早之前,还没有Linux Kernel的时候,是Unix的天下。Unix是一款开源的系统,很多开发者都基于Unix做各种定制开发并开源出来,一时间各种类Unix系统层出不穷,局面一度非常混乱。为了提升各版本系统的兼容性和支持跨平台开发,IEEE发布了POSIX标准。POSIX全称是Portable Operating System Interface for Computing Systems,它定义了具备可移植操作系统的各种标准,其中关于线程的标准参考:pthreads。目前包括Linux、Windows、macOS、iOS等系统都是兼容或部分兼容POSIX标准的。

Linux Kernel的早期版本并没有线程的概念,所有的任务都是通过进程管理调度的。Linux Kernel 2.0到2.4的版本,使用LinuxThread线程模型来实现对线程的支持。具体的和clone系统调用的flags参数有关:

#include <sched.h>
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
                 /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

如果flags中设置有CLONE_VM标志位,则表示和父进程使用相同的虚拟内存空间,即在用户态申请创建一个线程时,在内核中实际创建了一个进程,只不过这个进程和父进程共享虚拟内存空间。可知,使用LinuxThread线程模型时在一个进程的不同的线程中执行getpid获取到的值是不一样的,这与POSIX标准相悖。

为了兼容POSIX标准,glibc引入了由Redhat主导贡献的NPTL(Native POSIX Thread Library)的线程模型。同时为了支持NPTL,Linux Kernel 2.6也做了相应的调整。一个重要的改动是在进程信息结构体task_struct中新增tgid字段:

 struct task_struct {
 	...
 	pid_t pid;   //线程ID
 	pid_t tgid;	//线程组ID,即进程ID
 	struct task_struct *group_leader; //主线程指针
 	...
 }

当一个线程的pid和tgid相等时,这个线程就是组长线程,也就是常说的“主线程”,其pid就是这个线程组的进程号。而线程组的其它线程的tgid被设置为主线程的pid。使用getpid()获取到的是进程ID,实际上读取的是tgid字段。而gettid()返回的则是pid字段。NPTL同时在clone函数中新增CLONE_THREAD标志位,告诉内核需要创建的是线程,内核会将新线程的tgid设置为调用线程的pid并设置自己的线程ID。这样,线程ID就符合POSIX标准了。

可以看到,在Linux Kernel中无论是早期的LinuxThread还是现在使用的NPTL线程模型,内核并没有过多地区分线程和进程,线程就是所谓的轻量级的进程,它们在内核中都是使用task_struct结构。

我们平时使用的pthread_create、pthread_self属于用户态接口,包含在glibc中,而glibc是目前Linux系统上的标准C库。应用程序通过pthread_create创建一个线程,在内核中对应的就是NPTL线程。

由于存在用户态和内核态两层线程模型,自然存在两种线程ID。

  • gettid

从Linux kernel 2.4.11开始以系统调用的方式支持,并在glibc 2.3.0版本开始支持gettid的函数调用。gettid返回pid_t(int型)的线程ID,也就是上面提到的内核task_struct结构的pid字段。NPTL线程模型保证了每一个线程(进程)ID都是独有的,不会发生冲突。

pid_t ttid = syscall(SYS_gettid);

iOS/macOS未实现SYS_gettid的系统调用,总是返回-1。

  • pthread_self

由POSIX threads库提供的接口,返回pthread_t类型的线程句柄。pthread_t由pthread线程库分配和维护,仅能保证同一个进程中是唯一的。但POSIX标准并没有规定pthread_t的具体格式,不同系统中pthread_t的实现可能是不一样的。

以iOS/macOS为例:

// <sys/_pthread/_pthread_types.h>
typedef struct _opaque_pthread_t *__darwin_pthread_t;

// <sys/_pthread/_pthread_t.h>
struct __darwin_pthread_handler_rec {
	void (*__routine)(void *);	// Routine to call
	void *__arg;			// Argument to pass
	struct __darwin_pthread_handler_rec *__next;
};

// https://easeapi.com/blog/158-thread-id.html
struct _opaque_pthread_t {
	long __sig;
	struct __darwin_pthread_handler_rec  *__cleanup_stack;
	char __opaque[__PTHREAD_SIZE__];
};

typedef __darwin_pthread_t pthread_t;

可以看到,pthread_t是指向_opaque_pthread_t结构体的指针,结构体中的__darwin_pthread_handler_rec存储了线程需要调用的函数及参数。

iOS/macOS提供了pthread_threadid_np的扩展方法将pthread_t的线程句柄转换为整数的线程ID。

int pthread_threadid_np(pthread_t _Nullable,__uint64_t* _Nullable);

在iOS/macOS系统中,也可以使用SYS_thread_selfid的系统调用获取整数的线程ID,和pthread_threadid_np效果一样。

pid_t tid = syscall(SYS_thread_selfid);

而在Linux系统中,pthread_t在pthreadtypes.h中定义:

typedef unsigned long int pthread_t;

可以看到pthread_t就是数值型的线程ID。

参考:

man getttid
man pthread_self

其它文章

Linux线程局部存储 Thread Local Storage