Linux基础内容(23)—— 信号补充与多线程交接知识_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/131275661?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22131275661%22%2C%22source%22%3A%22m0_63488627%22%7D
目录
1.基本介绍
1.虚拟地址和页表的作用
2.页表的结构
2.认识线程
1.进程与线程笼统概念
2.Linux下的线程以及进程的重新认识
3.总结线程知识
4.演示进程
5.线程创建
1.共享资源
2.私有资源
3.线程实现反映的特性
1.进程vs线程
2.一般应用
3.缺陷
4.验证
1.基本介绍
1.虚拟地址和页表的作用
1.虚拟地址是进程能看到的资源窗口,进程能知道在虚拟地址内部每一个地址的含义
2.页表则是分配了进程真正能看到的资源,通过所谓的映射关系映射到物理内存中
3.合理的地址空间和页表对资源进行划分,使得我们能对进程的资源进行充分管理
2.页表的结构
1.在此之前我们还需要对物理内存的划分进行说明。如果单纯的将物理内存看为二进制,获取也是一个字节一个字节的获取,那么该性能不会体现出当代的电脑如此灵活。说明划分并不是简单的划分。
2.事实上,物理内存也是被操作系统管理的,既然要被管理就应该先被组织起来。物理内存被划分为以4KB为单位的页框,这样也是为了方便读取,节省时间。那划分出页框后,操作系统则将其管理成数据结构struct Page用于管理
3.那我们知道物理内存是4KB为单位的页框后,那么其来源的磁盘,数据从磁盘转移到物理内存中其实也是以4KB为单位转移的,不过要知道磁盘的4KB被叫做页帧
知道以上的空间管理后我们就能对页表有更深的理解了
1.其实页表也是一种管理的结构,既然是管理结构就绕不开属性,不过属性中不是只有内存而已,它还包括了命中判定,读写权限,内核态用户态状态判断的属性
2.虚拟地址由32位为例子。32位,说明地址有2的32次方中结果。我们的页表总不能映射2的32次方个以上面表为基础的结构吧,这样的地址空间直接消耗的比虚拟空间还要大,显然不现实
3.首先物理地址的32位被划分为10,10,12。其中第一个10位表示有2的10次方个页目录作为索引,将32位强行分为2的10次方个。第二个10位则是每一个页目录索引到另外2的10次方个的页表。随后页表指向物理内存的页框,页框以4KB划分,而2的12次方字节正好是4KB,这样就得到管理的最终结构了
2.认识线程
1.进程与线程笼统概念
1.最开始的理解对于进程就是所谓的运行的文件,该文件的属性和大小都是进程的一部分
2.task_struct结构通过页表映射找对需要被调度的函数。那么我们能引出线程的概念,如果我们需要不同的task_struct结构管理一个虚拟地址空间,通过划分来管理不同的执行部分,这样就得到了不同的执行流,而这些执行流就是所谓的线程。那么线程就是有进程划分出来,管理同一个虚拟地址空间的不同段的执行流。
3.而这种单个"进程"(线程)的执行力度是比之前描述的进程要细
2.Linux下的线程以及进程的重新认识
1.既然线程也是一种结构,那么我们就需要对它进行组织与管理。我们现在知道的是线程是进程划分出来的类似于子集。
2.对于windows来说,其实线程是单独做出一个数据结构来管理的;而Linux下,由于进程与线程的数据结构高度耦合,并且对于操作系统执行的角度来说,无论执行线程还是进程都是顺序的执行指定函数,所以Linux采用的是和进程一套数据结构
3.那么此时我们对线程和进程重新进行描述了。对于进程,虽说是执行的文件没错,但是在内核中看来,进程就是用来分配系统资源的基本实体,它包括了文件代码,系统的调用,cpu调用文件时的临时数据,线程,虚拟地址空间以及页表等
4.操作系统中的线程就是CPU真正调度的基本单位
5.由上面我们所理解的真正的CPU真正的调度的基本单位是线程,那么最之前所介绍的进程调度所有的信息和资源其实也是符合条件的,只是但是的场景下操作系统只有一个执行流,这个执行流就是我们最开始说的进程,其实也是线程。因为Liunx下不管进程和线程,只看其结构给出的进行服务调度
6.当前由于操作系统不关心线程还是进程,task_struct其实就是轻量级进程,Linux的线程就是轻量级进程。
3.总结线程知识
特点
1.Linux中没有真正意义的线程,它是将轻量化进程,是Linux自己处理线程的一个方案
2.不过在Linux看来,进程线程一视同仁
3.其好处其实是,简单,降低维护成本,可考虑高
缺点
1.操作系统设置的线程是轻量化进程,但是用户如果使用也只会考虑线程
2.Linux不能提供线程的系统调用接口因为根本没有,它只能提供轻量级进程
4.演示进程
thread:输入轻量级进程地址
attr:手动修改线程的属性配置
start_routine:线程执行的线程
arg:传入函数中的数据
void *thread_routine(void *args) { const char *name = (const char *)args; while (true) { cout << "我是新线程, 我正在运行! name: " << name << endl; sleep(1); } } int main() { // typedef unsigned long int pthread_t; pthread_t tid; int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread one"); assert(0 == n); (void)n; // 主线程 while (true) { // 地址 -> ? char tidbuffer[64]; snprintf(tidbuffer, sizeof(tidbuffer), "0x%x", tid); cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer << endl; sleep(1); } return 0; }
此时编译是通过不了的
这是因为,操作系统没有线程的真正意义,也就是说当前的pthread的创建不是操作系统的调用接口,而是库提供给用户使用的。所以我们需要在编译时将库动态链接进去。-lpthread
在用户和操作系统之间,已经提供了完整的库可以被调用
查看进程,我们发现,只有一个pthread的进程,跟我创造了两个轻量级进程完全没有对应上
ps -aL才是查看线程的,LWP表示轻量级进程
此时我们会发现有一个线程PID和LWP是完全一样的线程,该线程为主线程,其余的相同CMD的线程都是子线程。那么此时我们就能兼容起进程的内容。操作系统不关心到底操作的是进程还是线程,我们kill进程其实就是kill掉只有主线程的PID,这个PID既是进程又是线程,操作系统kill的都是轻量级进程,不过在这样的设计下一视同仁。
5.线程创建
1.共享资源
一般而言,几乎所有的资源都被所有的资源所共享
1.函数
2.全局变量
包括文件描述符表,每种信号的处理方式,当前工作目录,用户id和组id
int g_val = 0; std::string fun() { return "我是一个独立的方法"; } void *thread_routine(void *args) { const char *name = (const char *)args; while (true) { fun(); cout << "我是新线程name: " << name << " : "<< fun() << " : " << g_val++ << " &g_val : " << &g_val << endl; sleep(1); } } int main() { pthread_t tid; int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread one"); assert(0 == n); (void)n; while (true) { char tidbuffer[64]; snprintf(tidbuffer, sizeof(tidbuffer), "0x%x", tid); cout << "我是主线程 tid: " << tidbuffer << " : " << g_val << " &g_val : " << &g_val << endl; sleep(1); } return 0; }
2.私有资源
哪部分资源是各个线程独有的?
1.PCB属性私有,因为每一个轻量级进程都是独立
2.轻量级线程相互转换运行,动态运行,其中的上下文结构是独立的
3.有独立的栈结构,用于保存所有的临时变量,有属于自己的私有数据
包括线程ID,一组寄存器,栈,errno,信号屏蔽字,调度优先级
3.线程实现反映的特性
1.进程vs线程
1.进程的创建比线程的创建要麻烦。由于创造进程需要将虚拟地址空间,页表,寄存器上下文等数据全部拷贝,所用的成本变高。而线程只需要创造一个线程即可。
2.进程的来回切换比起线程的来回切换耗费工作量多。
进程切换需要切换页表,虚拟地址空间,切换PCB和上下文;
线程只需要切换PCB和上下文;
上面的切换页表和虚拟地址空间不占消耗工作量的大头,在CPU中,为了使得寄存器和内存之间的拷贝效率更高,其中会有cache(高速缓存)来进行缓冲,而针对线程,绝大多数的资源是共享的,意味着cache中的热点数据几乎相同,那么切换时cache不需要完全更新;对于进程就不一样了,由于全部都需要更新,工作量就更高
2.一般应用
1.针对计算密集型应用(多访问CPU资源),通过分解出多线程
2.针对I/O密集型应用(多访问IO资源),用不同的线程进行I/O操作
3.缺陷
1.性能损失:线程最好与核数相互匹配,核指的是cpu中运算单元的个数。如果计算密集型
线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。进程则是看cpu数
2.健壮性降低:进程退出不影响其他进程,而线程退出会影响其他线程3.缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4.编程难度提高:编写与调试一个多线程程序比单线程程序困难得多4.验证
健壮性问题
void *start_routine(void *args) { string name = static_cast<const char *>(args); // 安全的强制类型转换 while (true) { cout << "new thread success, name: " << name << endl; sleep(1); int* p = nullptr; *p=0; } } int main() { pthread_t id; pthread_create(&id, nullptr, start_routine, (void *)"thread new"); while (true) { cout << "new thread success, name: main thread"<< endl; sleep(1); } return 0; }
运行该代码,主线程是没有问题的,只是子线程中指针非法访问。那么整个进程就会因为线程的问题而整体退出。这是因为信号其实是进程的信号,那么接收信号的实体也是进程,而线程的错误也意味着进程的错误,进程发生错误被回收,同理线程也会一并被回收,这样也就解释了为什么健壮性不强,因为动一发而牵全身。
轻量级进程接口