Thread-local storage线程本地储存

线程本地存储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_THREADLinux 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之后,子进程和父进程共享相同的文件描述符表

测试方法:

  1. 创建一个txt文件
  2. 创建一个全局变量:txt文件的文件描述符
    在这里插入图片描述
  3. clone两个子进程,一个有flag添加CLONE_FILES一个没有。
  4. 在两个子进程中直接读取上述txt文件的内容,能读出txt内容的就是与父进程共享文件描述符表的。
    在这里插入图片描述
    在这里插入图片描述
CLONE_SIGHAND

使用了CLONE_SIGHAND信号之后父子进程共享向量处理表。

测试方法:

  1. 定义一个全局的信号结构体
  2. 具体的信号设置放在调用进程中
  3. clone两个进程一个加了CLONE_SIGHAND信号,另一个没有
  4. 重点:将信号处理函数的配置放在两个不同的clone进程中,两个进程分别调用不同的信号处理函数
    在这里插入图片描述
    在这里插入图片描述

两个处理函数的主要区别是:
在这里插入图片描述

运行结果:
在这里插入图片描述

CLONE_NEWIPC

加入CLONE_ NEWIPC之后,将在新的IPC命名空间中创建子进程

在这里插入图片描述
child1加入了CLONE_NEWIPC而child2没有。
在这里插入图片描述
在两个子进程分别调用ipcs系统调用,展示进程间通信方式的信息。
运行结果(须在特权模式下编译运行):
在这里插入图片描述
由运行结果我们可以发现,两个子进程处于两个不同的IPC命名空间中。

CLONE_THREAD

加入了CLONE_THREAD之后,Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群
即使用CLONE_THREAD之后,父子进程将被放在同一线程组中,二者pid相同但是tid不同。

在这里插入图片描述
在这里插入图片描述

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值