速贝cms建站系统,温州网站升级,网站建设大体包含,钓鱼网站的主要危害一、线程概念
1、如何理解线程
说到线程#xff0c;那么我们就要回到进程了。
1.1. 再谈进程
对一个进程来说#xff0c;它在内存中是这样的#xff1a; 图1.1-a 其中一个 task_struct 独享一个进程地址空间和一个页表。 而线程其实和进程差不多#xff0c;是这样的那么我们就要回到进程了。
1.1. 再谈进程
对一个进程来说它在内存中是这样的 图1.1-a 其中一个 task_struct 独享一个进程地址空间和一个页表。 而线程其实和进程差不多是这样的 图 1.1-b 其中多个 task_struct 共享一个进程地址空间即共享进程地址空间里的正文代码、堆、变量等。所以其实线程就是上图的一个个 task_struct 然后这一个个 task_struct 分别执行进程代码的不同部分。所以也可以说因为进程的执行是由若干个线程的执行组成的。而也正因为线程执行的代码比进程的要少得多因此我们称线程的执行粒度比进程要细得多。
所以那真正的进程是什么样的呢其实是这样的 图 1.1-c 但是我们或许会产生这样一个疑问那是不是以前进程的那些定义都错了呢一个进程不是只有一个 PCB 吗而在 Linux 里不就叫 task_struct 吗其实这两者是不矛盾的其实在只有一个 task_struct 的情况下这个 task_struct 也是一个线程只不过因为只有它一个因此刚好独享了进程地址空间的全部资源罢了所以进程的图就是图 1.1-a 样子了。所以其实图 1.1-c 才是更普适的情况。
1.2. 理解线程
如果我们新建一个进程包括 fork 一个子进程那么操作系统就要为这个新进程开辟新的 task_struct、进程地址空间和页表。而如果我们新建一个线程那么操作系统就只需要创建一个新的 task_struct 就可以了然后和旧的 task_struct 一起共享页表和进程地址空间然后不同的 task_struct 各自执行不同部分的代码。
所以新建一个线程操作系统除了会开辟空间当作 task_struct 外就不会额外开辟其他空间了但是新建一个进程操作系统就会开辟新的 task_struct、进程地址空间和页表简称新的系统资源。所以我们可以得出一个结论操作系统执行代码是通过一个一个线程执行的但操作系统只会对一个个进程分配系统资源内存并不会对一个个线程分配系统资源内存。
1.3. 线程与进程的关系
因为进程的执行是由若干个线程的执行组成的但是操作系统是对进程发信号的因此如果其中一个线程报错发信号那么就会导致整个进程退出。那么既然整个进程都退出了那进程里的全部线程绝对也全都退出了。所以我们可以得出一个结论只要有一个线程挂了那么整个进程里的线程也会全部挂掉。
2、二级页表32 位
当机器字长为 32 位时内存里的地址只能用 32 位表示。但是我们知道如果进程地址空间只用了一级页表储存映射关系的话而且进程地址空间总共是 4GB因此页表的总大小也会是 4GB。可是这只是一个进程的而且只是页表的大小还没算代码和数据呢如果算上这些的话那么一个进程所占的空间就太大了操作系统是不可能支持几十个进程载入内存的。所以如何缩小页表的大小呢那么我们就要说说二级页表了。
2.1. 重新看待 32 位地址号
二级页表对 32 位地址号进行了拆分采用了 10-10-12 的格式。高 10 位是指在哪一个二级页表中 10 位是指在这个二级页表的哪一个内存块而低 12 位是指这个内存块内的哪一行。 二级页表 举个例子如果地址为 0000000111 0000001000 000100101100那么就说明要访问第 7 号二级页表中的第 8 号个内存块的第 300 号行。注意所有的编号都是从零开始的。
2.2. 回到二级页表
得益于对 32 位地址的分段解读虽然全部的二级页表的总行数还是和原来不用二级页表时的页表一样多但通过二级页表我们可以把原来的一级页表分成若干段然后需要时就只把那一段的表加载进内存就行了而不用像原来那样把整张一级页表加载进内存。这样就可以大幅减少页表所占用的内存空间了。
同时我们还可以发现正是因为用了 10 位来表示表一级页表和二级页表中的编号使得每个表的大小都是 4KB——刚好可以用一个内存块就可以装满了。因此操作系统开任何空间的大小都是按 4KB 的大小来开的只不过我们可以用到的就不一定是 4KB 了。
3、线程周边概念
3.1. 执行流
因为 Linux 中CPU 只认 task_struct并不会认是不是进程或是不是线程因此操作系统是没有进程或线程这一概念的它只认 task_struct 因此一个 task_struct 就是一个执行流了。
3.2. 主线程 新线程 其中 LWP轻量型进程等于 PID 的那个线程就是主线程其他都是新线程。
二、线程的控制
1、线程的创建
1.1. clone 函数 参数介绍 fn新线程要执行那部分的代码的起始地址。stack 该线程的线程栈用于存储该线程的数据和变量以及该线程的函数的栈帧。 这个函数是用来新建线程的就如同 fork 函数一样。 但是由于这个函数的参数是在太复杂了因此线程库就把这个函数封装了起来即在 pthread_create 函数和 pthread_join 函数的内部调用 clone 函数。
1.2. pthread_create 函数 参数介绍 thread输出型参数。把线程 ID 带出来。attr输入型参数。输入该线程的属性如果不输入就用默认属性。start_routine就是 clone 函数的 fn。arg输入型参数。输入 start_routine 函数的实参。 这个函数就是用来创建线程的。成功返回 0失败返回其他值。
1.3. 线程 IDpthread_t
1.3.1. 为什么线程库要维护线程的概念
为什么线程库只要维护线程的概念而不用维护线程的执行流task_struct因为用户在用线程时就指想获取线程有关的属性比如线程 ID、线程的时间片、回调函数、独立栈等但并不需要关注 task_struct 的编号、PID等那些 task_struct 的信息因此线程库就没必要包含与 task_struct 的属性有关的东西了不用描述 task_struct 了取而代之的是为了实现线程与执行流的概念线程库中会把 tcb 与执行流建立连接然后当线程要执行它的代码时操作系统直接调用它的执行流就行了但值得注意的是此时用户并不关心操作系统调用的执行流只会关心线程的属性
1.3.2. 线程栈
由 clone 函数可知创建一个线程时不仅需要执行代码的起始地址还要传一个栈的地址进去。而这个栈就是线程栈。
1.3.3. 线程 tid 线程控制块 tcb
因为 Linux 是没有线程的概念的只有 task_struct 的概念因此在线程库内部就要维护线程的概念而要维护线程的概念就必须对多个线程进行管理于是就要对线程进行“先描述再组织”。而如何描述一个线程呢其实 clone 函数和 pthread_create 函数就已经给出答案了每个线程都要有自己的 ID以及其他属性还有自己的独享的栈。但这些都是在库内部维护的因此如果要访问这些线程那么就要把库加载到内存但线程库是动态库因此经过页表映射后是映射到进程地址空间的共享区里的而线程这些概念是线程库在描述和组织是线程库里的代码因此线程的结构体和组织线程的数据结构、以及线程的栈都被加载到共享区里了而不是进程地址空间的栈里。但主线程非常特殊它的栈就是进程地址空间里的栈。
因为一个线程是由线程的属性权限、时间片等和线程栈组成的因此在描述线程的结构体 tcb 肯定也是有这几个成分的。因此 tcb 的结构大概长这样 而 tcb 的地址就是这个线程的 tid。
1.3.4. 线程的局部存储—— __thread 关键字
我们知道全部线程是可以访问全局变量的。如果我线程想要一个私有的全局变量呢那就要提到 __thread 关键字了。
__thread int count 0; // 每个线程都有一个私有的 count 全局变量
以上面的例子为例有了 __thread 之后count 变量就不存在进程地址空间的数据区里了而是存在每个线程 TCB 的线程局部存储区里了。
或许我们会想那我在执行流的函数里定义个 count 变量不也一样吗至于用这个私有的全局变量吗其实这个私有的全局变量同时适用于一个线程要执行多个函数这种情况。在这种情况下该线程调用的所有函数都可以访问到这个 count但如果只在执行流的函数里定义个 count 变量该执行流调用的其他函数是无法访问这个 count 的。 注意__thread 只能用于内置类型。 2、线程的回收
2.1. pthread_join 函数 参数介绍 thread输入型参数。线程的 tidvalue_ptr 输出型参数。用来获取线程执行的函数的结果。如何获取呢一般都会把线程执行的函数先强转成void*指针再返回。然后在给这个形参传 void* 指针变量的地址。返回值成功返回 0错误返回其他值。 举个例子
void* thread_run(void* arg)
{return (void*)100;
}int main()
{pthread_t tid;pthread_create(tid, nullptr, thread_run, nullptr);void* ret_val;pthread_join(tid, ret_val);int ret static_castint(ret_val);return 0;
} 这样就可以获取线程执行的函数的结果啦
2.2. 线程分离—— pthread_detach 函数
在默认情况下新建的线程的释放都要显式调用 pthread_join 函数。但如果我们不关心线程运行的结果我们可以在线程执行的函数调 pthread_detach 函数告诉系统当线程执行完后可以自动回收线程。于是我们就不用显式地调用 pthread_join 函数来释放线程了。 参数介绍 thread线程的 tid。返回值成功返回 0失败返回其他值。