同一进程中的共享和私有数据///

由一个题引发的小思考:
这里写图片描述

用一进程中线程不共享的部分应该是什嘛啊!!!

首先声明一点:
线程堆栈:简称栈 Stack
托管堆: 简称堆 Heap

线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。

进程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:

1,线程ID
 每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。

2,寄存器组的值
由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上 时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。

3,线程的堆栈(即就是:栈)
堆栈是保证线程独立运行所必须的。
线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈, 使得函数调用可以正常执行,不受其他线程的影响。
这里写图片描述

一个线程范围的私有全局变量,即使改变也不会对其它线程产生影响。POSIX通过维护一定的数据结构来解决这个问题,这个数据结构为:TSD(Pthread_Specific Data)线程特定数据

/*
总共有128项,是通过key-> value的形式来访问的,默认情况

下,每个线程都有这128项,那么我们要使用其中的一项,我们首先要先创

建一个key,首先从128项中去查找,这些指针是不是都是空的,如果是

空的话,那么说明我们找到了一个空的位置,我们就可以创建一个key了)

  假如我们的线程中用的是key[1],也就是说:一个线程创建了一个

key,那么其它的线程也得到了一个key,也就是说:创建的这个key

指针相当于全局指针变量,供所有的线程用。但是,这个全局指针变量指向的

实际数据是不一样的,每个线程都有一个自己的特有数据,指向了不同的内存

这样的话,也就说明,一个线程改变这个数据并不会影响其它的线程的使用

啊,,,,
  */

4,错误返回码
由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。
所以,不同的线程应该拥有自己的错误返回码变量。

5,线程的信号屏蔽码
由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。

6,线程的优先级
由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

涉及多线程程序涉及的时候经常会出现一些令人难以思议的事情,用堆和栈分配一个变量可能在以后的执行中产生意想不到的结果,而这个结果的表现就是内存的非法被访问,导致内存的内容被更改。

在一个进程的线程共享堆区,而进程中的线程各自维持自己堆栈。

在 windows 等平台上,不同线程缺省使用同一个堆,所以用 C 的 malloc (或者 windows 的 GlobalAlloc)分配内存的时候是使用了同步保护的。如果没有同步保护,在两个线程同时执行内存操作的时候会产生竞争条件,可能导致堆内内存管理混乱。比如两个线程分配了统一块内存地址,空闲链表指针错误等。

Symbian 的线程一般使用独立的堆空间。这样每个线程可以直接在自己的堆里分配和释放,可以减少同步所引入的开销。当线程退出的时候,系统直接回收线程的堆空间,线程内没有释放的内存空间也不会造成进程内的内存泄漏。

但是两个线程使用共用堆的时候,就必须用 critical section 或者 mutex 进行同步保护。否则程序崩溃时早晚的事。

进程内核栈

  我们在编程的时候知道,在进程地址空间中有个栈,用于程序的顺利执行,而当程序陷入内核态之后,就不能够使用应用态的栈了,所以,对于每个进程,它在内核中也有一个内核态的栈区,在内核中,把栈和thread_info(线程描述符)结构结合起来放在一起,这块存储区域通常为8192字节,也就是两个页框。thread_info结构大小为52字节,也就是说,进程的可用的栈大小为8140个字节。因为进程在内核态中所需要执行的代码量并不算多,所以这个8K的内核栈已经足够使用。在编译内核时也可以设置整个内核栈为一个页框大小(4KB),不过在这种情况下,内核在处理硬中断和软中断时就不使用进程的内核栈,而是使用额外的两个个栈:硬中断请求栈(每个CPU一个,大小4K),软中断请求栈(每个CPU一个,大小4K)。不过值得注意的是,在进行异常处理时还是会使用进程的内核栈。

这里写图片描述
如上图可以看到,进程的内核栈是向下增长的,也就是栈底在高位地址,栈顶在低位地址。对于这个内核栈的作用,我们可以总结一下:

进程陷入内核后用于代替应用层的栈区进行使用。
中断发生时用于保存进程上下文现场,并且用于中断嵌套的现场保存和返回。
当发生进程切换时,部分寄存器的值会保存在进程的内核栈中。
thread_info中保存着一些重要的字段用于维持进程的正常运行。

PID和tgid字段

  PID是一个数字,用于标识一个进程,就像学生的学号一样,每个进程都有一个唯一的编号,保存在进程描述符的pid字段中。一般的,在系统运行期间,PID都是被顺序编号,比如进程A的PID为10,那下个创建的进程的PID则为11。不过PID的值有一个上限,当内核使用的PID达到这个上限后就会循环开始找已闲置的小PID号。在缺省状态下,最大PID值为32767(PID_MAX_DEFAULT - 1);可以通过修改/proc/sys/kernel/pid_max这个文件来减小PID上限值。而在64位系统中,PID可扩大到4194303。

  内核是通过一个叫pidmap的位图来管理已分配的PID号和闲置的PID号。在32位系统中,pidmap的大小就是一个页框的大小(4KB),而一个页框大小为32768位,也就是每一位代表一个PID号,1代表此PID已经被分配,0代表此PID号未被使用;而在64位系统下,pidmap会使用多个页框。

  在POSIX标准中规定了一个多线程应用程序中所有的线程都必须有相同的PID,在linux内核中,是使用轻量级进程实现线程的功能,但是轻量级进程也是一个进程,他们的PID都不相同,为了实现这一点,内核在进程描述符中引入了tgid字段。在linux的线程组概念中,一个线程组中所有线程使用的该线程组领头线程相同的PID,也就是该组第一个轻量级进程的PID,并保存到进程描述符的tgid字段中,如下图:

这里写图片描述

在编程过程中,我们使用的getpid()函数返回的值其实是当前进程的tgid而不是pid的值,而由于线程组中领头线程和pid和tgid相同,因而getpid()对这类进程所起到的作用和一般进程是一样的。

  接下来说说内核如何将所有的PID和进程描述符组织在一起,方便系统查找和使用。在系统运行过程中,可能会有成百上千的进程在运行,这时候进程的查找效率就至关重要了,比如系统管理员使用kill 1024命令去终止PID=1024的进程,内核会从这个PID导出对应的进程描述符进行处理。内核为了提高查找效率,专门使用了4个哈希表用于索引进程描述符。为什么要4个,因为我们可以用pid、tgid、pgrp、session去找进程,这几个哈希表说明如下:

这里写图片描述

在内核中,这四个哈希表一共占16个页框,也就是每个哈希表占4个页框,他们每个可以拥有2048个表项,内核会把把这四个哈希表的地址保存到pid_hash数组中。现在问题来了,拿pid的哈希表为例,怎么在2048个表项中保存32767个PID值,其实内核会对每个已经分配的PID值进行一个处理,得到的结果的数值就是对应的表项,处理结果相同的PID被串成一个链表,如下:

这里写图片描述

当我们使用kill 29384命令时,内核会根据29384处理得出199,然后以199为下标,获取PID哈希表中对应的链表头,并在此链表中找出PID=29384的进程。进程描述符中使用struct pid_link pids[PIDTYPE_MAX]链入这四个哈希表。对于另外三个哈希表,道理一样。

进程间关系

  在系统中,除了进程0,一个进程是由另一个进程创建,它们都具有父子关系。如果一个进程创建多个子进程,则子进程之间有兄弟关系。在整个系统启动期间,会初始化系统的第一个进程init_task,这个进程属于内核中的一个进程,它算是所有进程的祖先,之后它会启动PID为1的init进程和PID为2的kthreadd,这两个进程之后启动的所有进程,而init_task之后会转变为一个idle进程用于CPU空闲时运行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值