线程本地存储TLS
线程本地储存又叫线程局部存储,是线程私有的全局变量。
普通的全局变量在多线程中是共享的,当一个线程对其进行更改时,所有线程都可以看到这个改变。而线程私有的全局变量不同,线程私有全局变量是线程的私有财产,每个线程都有自己的一份副本,某个线程对其所做的修改只会修改到自己的副本,并不会对其他线程的副本造成影响。
有两种方式实现tls:
- __thread int tlsvar:tlsvar是每个线程各自的独立变量
- pthread_key_create
由__thread int 实现的tls
__thread int 方法其实就是直接指定一个变量,使其成为线程私有的全局变量。
__thread int tlsvar = 0;
即指定了tlsvar变量为线程私有的全局变量,也就是tls。
实现细节解释
可以看到,在同一时间段,虽然三个线程用的都是tlsvar变量,但是打印出来的值不同。说明三个线程都有各自的tlsvar,也就是线程私有的全局变量。
由pthread_key_create实现的tls
pthread_key_create
用来创建线程私有数据。该函数从 TSD 池中分配一项,将其地址值赋给 key 供以后访问使用。第 2 个参数是一个销毁函数,它是可选的,可以为 NULL,为 NULL 时,则系统调用默认的销毁函数进行相关的数据注销。如果不为空,则在线程退出时(调用 pthread_exit() 函数)时将以 key 锁关联的数据作为参数调用它,以释放分配的缓冲区,或是关闭文件流等。
不论哪个线程调用了pthread_key_create()
,所创建的 key 都是所有线程可以访问的,但各个线程可以根据自己的需要往 key 中填入不同的值,相当于提供了一个同名而不同值的全局变量(这个全局变量相对于拥有这个变量的线程来说)。
注销一个 TSD (Thread-specific Data,线程私有数据)使用pthread_key_delete()
函数。该函数并不检查当前是否有线程正在使用该 TSD,也不会调用清理函数(destructor function),而只是将 TSD 释放以供下一次调用 pthread_key_create() 使用。在 LinuxThread 中,它还会将与之相关的线程数据项设置为 NULL。一个进程最多可以创建1024个key
pthread_key_Create()
函数原型:int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
函数作用:用来创建线程私有数据。该函数从TSD(线程私有数据)池中分配一项,将其地址赋给key供以后访问使用。第二个参数是一个销毁函数,是可选的:可以为NULL,为NULL时系统默认的销毁函数进行相应的数据注销。如果不为NULL,线程退出时(调用pthread_exit()函数时)将调用这个指定的函数来释放绑定在这个key上的内存块。
pthread_setspecific()
函数原型:int pthread_setspecific( pthread_key_t key , const void * value);
函数作用:将value的值与key相关联,即将value写入该线程的TSD
void * pthread_getspecific( pthread_key_t key);
函数作用:将与key关联的数据读出来。因为返回数据类型为void*因此可以指向任何类型的数据。
int pthread_key_delete(pthread_key_t key);
函数作用:销毁线程特定数据键,由于键已无效,因此将释放与该键关联的所有内存。
创建完tls_key后,每个线程都会有一个属于自己的TSD,在线程中调用一个其他的函数,该函数的内存空间范围在线程中。
直接声明数据是在线程栈中申请的空间,使用malloc的话是在进程堆中申请的空间。
在线程中,调用一个函数,该函数声明了一个数据结构,并未使用malloc的话该数据结构申请在线程栈中,当函数返回线程时,该栈空间会被回收,数据也就不见了。
同样的,在线程中,调用一个函数,该函数使用malloc声明了一个数据结构,则该数据结构存在在进程堆空间中,当函数返回时该空间不会被回收。
实现细节
14-2
14-3
14-4
由输出结果我们可以看到,虽然两个函数都是调用了print函数,而且没有传任何参数直接在print函数中使用key,最后打印出来的结果没有变化。
原因是线程调用print函数的时候,print函数在线程的内存空间中,key是与该线程相关联的。
14-5
clone()
进行clone()系统调用时将创建一个新任务。但是不是复制所有数据结构,而是根据传递给clone()的标志集,新任务的task_struct的指针指向父级的真实或复制的数据结构。
函数原型:int clone(int (*fn)(void *), void *child_stack, int flags, void *arg,.../*pid_t* ptid, struct user_desc* tls, pid_t* ctid*/);
函数说明:这实际上是一个位于基础clone()系统调用之上的库函数。
函数作用:主要作用是实现线程,程序中的多个控制线程在共享内存空间中同时运行
参数说明
- fn():当使用clone()创建子进程时,它执行函数fn(arg)。当fn(arg)函数应用程序返回时,子进程终止。fn()返回的正式时子进程的退出代码。子进程也可以调用exit或在接收到致命信号后显式终止。
- child_stack:指定子进程使用的堆栈的位置。由于子进程和调用进程可能共享内存,因此子进程无法与调用进程在同一堆栈中执行,所以,必须为子进程设置内存空间,并将指向该空间的指针传给clone()。
注意:在堆栈中,所有运行linux的处理器都是从上到下增长的,因此chile_stack通常指向为子堆栈设置的内存空间的最高地址。
- flags:标志地低字节包含当子进程终止时发送给父进程的终止信号的编号。
如果将此信号指定为SIGCHLD以外的任何其他信号,则在使用wait()等待子进程时,父进程必须指定__WALL或者__WCLONE选项。如果未指定任何信号,则子进程终止时不通知父进程。
比较pthread和clone()线程实现机制的异同
不同点
Pthread是基于用户级线程来实现的,而clone()是基于轻量级进程(LWP)来实现的。
用户级线程是由用户空间运行线程来实现的,线程的创建等操作都是由线程库来完成的,不需要太多的系统调用,内核感知不到多线程的存在。
clone()创建的是轻量级线程LWP,是内核支持的用户线程。也就是说,LWP的创建的等操作都是需要经过内核的系统调用的。
调度
pthread所创建的线程依赖于该进程实现的算法来调度,调度算法可以自己实现。
而clone创建的LWP由内核调度。如果是一对一的线程模型,LWP就类似于用户线程。
相同点
在linux中,没有线程的概念,只有task。所以,在Linux环境下,无论是pthread还是clone()所创建出来的线程,在linux系统中被统一看作是一个task。实际上可以看做是clone()的调用加上标志的设置,开辟了寄存器空间,栈空间,以及私有存储空间。
FALGS总结
标志 | 含义 |
---|---|
CLONE_PARENT | 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子” |
CLONE_FS | 子进程与父进程共享相同的文件系统,包括root、当前目录、umask |
CLONE_FILES | 子进程与父进程共享相同的文件描述符(file descriptor)表 |
CLONE_NEWNS | 在新的namespace启动子进程,namespace描述了进程的文件hierarchy |
CLONE_SIGHAND | 子进程与父进程共享相同的信号处理(signal handler)表 |
CLONE_PTRACE | 若父进程被trace,子进程也被trace |
CLONE_VFORK | 父进程被挂起,直至子进程释放虚拟内存资源 |
CLONE_VM | 子进程与父进程运行于相同的内存空间 |
CLONE_PID | 子进程在创建时PID与父进程一致 |
CLONE_THREAD | Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群 |
CLONE_NEWIPC | 在新的IPC命名空间中创建子进程 |
对clone()的flags采用不同的配置,设计测试程序讨论其结果
CLONE_PARENT
在程序14-6的基础上修改:
在该线程的创建中,flag加入了CLONE_PARENT参数。
输出结果:
可以看到,新创建的线程的parent id跟调用进程的parent id相同,所以二者是兄弟。
CLONE_VM
运行14-6程序时加上vm参数
由parent的buf被修改我们可以知道的是子进程的buf和调用进程的buf在同一内存空间,所以而这共享内存空间
CLONE_VFORK
如果父进程不被挂起,那么在父进程调用完clone()之后会立即打印"parent waiting…"。
CLONE_FILES
加入CLONE_FILES之后,子进程和父进程共享相同的文件描述符表
测试方法:
- 创建一个txt文件
- 创建一个全局变量:txt文件的文件描述符
- clone两个子进程,一个有flag添加CLONE_FILES一个没有。
- 在两个子进程中直接读取上述txt文件的内容,能读出txt内容的就是与父进程共享文件描述符表的。
CLONE_SIGHAND
使用了CLONE_SIGHAND信号之后父子进程共享向量处理表。
测试方法:
- 定义一个全局的信号结构体
- 具体的信号设置放在调用进程中
- clone两个进程一个加了CLONE_SIGHAND信号,另一个没有
- 重点:将信号处理函数的配置放在两个不同的clone进程中,两个进程分别调用不同的信号处理函数
两个处理函数的主要区别是:
运行结果:
CLONE_NEWIPC
加入CLONE_ NEWIPC之后,将在新的IPC命名空间中创建子进程
child1加入了CLONE_NEWIPC而child2没有。
在两个子进程分别调用ipcs系统调用,展示进程间通信方式的信息。
运行结果(须在特权模式下编译运行):
由运行结果我们可以发现,两个子进程处于两个不同的IPC命名空间中。
CLONE_THREAD
加入了CLONE_THREAD之后,Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群
即使用CLONE_THREAD之后,父子进程将被放在同一线程组中,二者pid相同但是tid不同。