代做网站的好处,如何给自己公司设计logo,wordpress幻灯片 设置方法,wordpress手机版网页目录
1. 页表详解
1.1 权限条目页框
1.2 页目录页表项
2. 线程的概念
2.1 轻量级进程
2.2 Linux的线程
2.3 pthread_create
2.4 原生线程库LWP和PID
3. 线程的公有资源和私有资源
3.1 线程的公有资源
3.2 线程的私有资源
4. 线程的优缺点
4.1 线程的优点
4.2 线程…目录
1. 页表详解
1.1 权限条目页框
1.2 页目录页表项
2. 线程的概念
2.1 轻量级进程
2.2 Linux的线程
2.3 pthread_create
2.4 原生线程库LWP和PID
3. 线程的公有资源和私有资源
3.1 线程的公有资源
3.2 线程的私有资源
4. 线程的优缺点
4.1 线程的优点
4.2 线程的缺点
本篇完。 1. 页表详解
我们在之前一直都提到页表知道它的作用是将虚拟地址映射到物理地址但是它具体怎么映射的它的结构是什么样的并没有提及过。
char* str hello world;
*str H;上诉代码会在运行时报错原因是str指向的地址在字符常量区字符常量区的内容是不允许用户去修改的。
代码在运行起来以后操作系统是怎么知道用户在修改字符常量区的呢
1.1 权限条目页框 如上图所示的页表示意图页表中不仅右虚拟地址和物理地址的映射关系还有是否命中RWX权限U/K权限等等内容。
U/K权限U表示用户(user)K表示内核(kernal)。RWX权限当前身份(用户或者内核)对当前地址的读写执行权限。
上面代码在对srt指向的地址写内容时先会经过页表映射到物理地址。但是在页表中发现这是一个写操作并且该地址是不允许被写的此时MMU就发送信号导致程序报错。 虚拟地址物理地址以及属性所在的一行称为条目。 依然是这张图需要将这张图分解进行讲解。
物理内存空间划分 以32位系统为例它的物理内存理论上有4GB大小但是这4GB又被分成了多块小空间。 被分割的小空间每一块大小是4KB被叫做页框。 物理内存中会又很多个页框并且这些页框也需要操作系统管理起来采用的方式同样是先描述再组织。
通过一个结构体来描述页框
struct_Page
{//内存属性--4KB
}代码形式如上所示每一个页框都会有这样一个结构体对象将多个结构体对象放在一个数组中
struct_Page mem[];此时就完成了先描述再组织的过程。操作系统中有专门的内存管理算法叫做伙伴系统有兴趣的可以自行查阅资料了解。
可执行文件
写好的代码会经过编译器的处理形成二进制可执行文件放在磁盘中在运行的时候加载到内存中。 编译器在处理源文件生成的二进制可执行文件同样是以4KB为单位的这4KB的数据块被叫做页桢。 这一切都是设计好的所以可执行程序在加载到内存中的时候是以4KB为单位的正好一个页帧来填充一个页框。当页框被填充了以后就会创建对应的struct_Page结构体对象并且放在数组中让操作系统来管理。 1.2 页目录页表项
再回到页表我们知道每个进程对应的虚拟地址空间大小都是4GB的也就是有2^32个地址如果每个虚拟地址在页表中都对应着一个物理地址 那么页表就会有2^32行每一行都是一个条目每个条目中不仅有物理地址虚拟地址还有其他属性假设一个条目的大小是10B那么光页表就有10*2^3240GB已经超过了物理内存的大小所以页表肯定不是这样的。
实际上页表是由页目录和页表项组成的。
在32位机器上地址的大小是4G字节也就是有32个比特位
将32个比特位分为10个比特位10个比特位12个比特位共3组 之前的的页表 页目录 页表项如上图所示。 32个比特位的高10位作为页目录的下标如上图所示的0000 0000通过这个下标0可以访问到页目录中的第一个条目。页目录中存放的是页表项的地址可以通过下标找到对应的页表项。
10个比特位意味着页目录的下标范围是0~1023最多是210也就是1KB个条目大大减少了对内存的消耗。
32个比特位的中间10位作为页表项的下标同样可以访问页表项中的条目。页表项中存放的是物理内存中页框的起始地址可以通过下标找到物理内存中对应的页框。
同样一个页表项最多有1KB个条目指向1KB个页框。
32个比特位中的低12位作为偏移量在物理内存中页框的起始地址基础上进行偏移此时就可以得到具体数据在内存中的地址。这也是为什么页框和页帧的大小设置为4KB的原因因为最低的12个比特位是2^124KB偏移量最大就是4KB。
32位虚拟地址-物理地址的映射过程
根据高10位下标找到页目录中对应页表项的地址再根据中间10位下标找到页表相中对应页框的物理地址再根据低12位偏移量进行偏移找到具体的物理地址。
页目录和页表项同样是采用先描述再组织的方式被操作系统管理起来的每创建一个进程就会有一个页目录只有在目录中存在的页表项才会被建立。
采用这种方式大大减少了对内存的消耗。
如何看到地址空间和页表
地址空间是进程看到的资源窗口每个进程都认为自己有4GB的资源。页表决定进程真正拥有的资源。合理对地址空间和页表进行资源划分就可以对一个进程所有的资源进行分类。 2. 线程的概念 线程是进程中的一个执行流。 Linux线程是CPU调度的基本单位。 回忆一下之前我们对进程的定义是内核数据结构 进程对应的代码和数据。 如上图所示此时我们创建了多个“子进程”。
新创建的“子进程”中的mm_struct* mm都指向父进程的虚拟地址空间。也就是说所有“子进程”和父进程共用一块虚拟地址空间。
父进程和父进程共用一块虚拟地址空间的“子进程”就叫做线程。
此时开始我们就将带引号的子进程叫做线程因为以前的父子进程在这里都不这么叫了。 线程的作用执行进程中的一部分代码。 线程在执行进程的部分代码时有点像父子进程执行同一份代码中不同部分区别在于线程和父进程使用的是同一份虚拟地址空间而父子进程使用的是两个独立的虚拟地址空间。 2.1 轻量级进程
从图中可以看到每个线程都也有一个task_struct结构体对象用来描述线程的属性(id状态优先级上下文栈等等)。那么线程要不要被操作系系统管理起来呢
答案是要的而且采用的方式同样是先描述再组织描述线程的task_struct结构体被叫做TCB–线程控制块是英文Thread Contral Block的首字母。
描述好了以后同样像PCB一样需要用链表组织起来进行管理并也和PCB一样有自己的管理算法。
但是TCB中的属性和PCB几乎一样管理TCB的数据结构和算法也和PCB的一样。 此时不仅会导致代码上的冗余而且还会增加系统的开销所以Linux并不是使用TCB管理线程的因为这种方式比较复杂维护起来不方便而且运行也不是很稳定。 Linux中线程是直接复用PCB的数据结构和管理方法。 所以在Liux操作系统中进程和线程的描述结构体都是task_struct。 站在CPU的角度它不关注被定义出来的进程还是线程这样的概念它只关注task_struct。 CPU是一个被动的硬件给它什么它就执行什么所以它并不会区分当前执行的task_struct是一个进程还是一个线程在它看来都是进程。
之前VS现在
之前CPU执行的task_struct是一个进程。现在CPU执行的task_struct是一个执行流。
所以说今天CPU处理的task_struct 小于或等于 之前task_struct的含义。 站在内核的角度称今天学习的task_struct为轻量级进程。 我们可以通过虚拟地址空间 页表的方式对进程进行资源划分让不同的“轻量级进程”同时执行不同部分的代码所以单个“轻量级进程”的执行粒度一定要比之前的进程细。 Linux内核中并没有线程的概念线程是用进程PCB来模拟的。 由于Linux中线程也是使用的PCB结构是一种轻量化的进程所以在Linux内核中并不存在线程的概念也不存在线程的结构。 站在CPU的角度每一个PCB都被称为轻量级进程。 CPU每次都是调度一个task_struct结构体而这些PCB都是轻量级进程有可能看作进程也有可能看作线程即使是看作进程也可以看作是一个线程因为无论是进程还是线程都是一个个的执行流CPU每次调度的都是一个执行流。 进程的重新定义进程是承担分配系统资源的基本实体。 每创建一个进程都会创建一个PCB一个虚拟地址空间一个页表一块物理空间而线程是属于这个进程中的执行流它使用的是这个进程的资源。
所以此时的进程就包括因为创建它而产生的一系列开销(PCB虚拟地址空间页表物理空间)这些都是属于这个进程的。 当这个进程中的某个线程申请新资源的时候也是以该进程的名义去申请而不是也这个线程的名义。 讲到这里是不是觉得和之前学习的进程概念有冲突了其实是自洽的。
之前我们学习的进程每个进程只有一个执行流。
而现在每个进程中有多个执行流每个执行流都是一个线程。
一个进程内可以有多个执行流这些执行流都共用一个虚拟地址一个页表。 最初的进程执行流被叫做主线程之后创建的执行流被叫做新线程。 主线程和新线程都属于一个进程都是一体的就像一个家庭中有不同的成员他们的工作是不同的但是总目的都是一样的。
同样多个线程同时工作的总目的也是相同的–为了完成这个进程的任务。 2.2 Linux的线程
通过上面介绍我们知道在Linux内核中是不存在线程这一个概念的因为没有TCB数据结构以及管理算法而我们所说的线程都是在宏观层面代指所有操作系统。 Linux操作系统中也没有提供创建线程的系统调用。 无论是宏观操作系统还是用户(程序员)都只认线程的概念但是Linux内核中并没有线程的概念。
我们(程序员)在编程的时候仍然会使用线程的概念那么我们在创建线程的时候Linux内核中是怎么创建出轻量级进程的呢 我们在创建进程的时候会调用一个线程库库中再通过一些系统调用创建出轻量级进程。
这样一来程序员创建线程Linux中创建轻量级进程双方的要求就都满足了。
这个线程库是所有Linux操作系统必须自带的所以也叫做原生线程库。 2.3 pthread_create
看看创建线程使用到的库函数接口man pthread_create pthread_t* thread线程标识符tid是一个输出型参数。const pthread_attr_t* attr线程属性当前阶段一律设成nullptr。void* (*start_routine)(void *)是一个函数指针线程执行的就是该函数中的代码。void* arg传给线程启动函数的参数是上面函数指针指向函数的形参。返回值线程创建成功返回0失败返回错误码。
先看一段pthread_create使用代码Makefile
mythread:mythread.ccg -o $ $^ -stdc11
.PHONY:clean
clean:rm -f mythread
mythread.cc
#include iostream
#include unistd.h
#include pthread.husing namespace std;void *threadRun(void *args)
{const string name (char*)args;while(true){cout name , pid: getpid() endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(tid, nullptr, threadRun, (void*)thread 1);while(true){cout main pthread pid: getpid() endl;sleep(1);}return 0;
} 将上诉代码进行编译的时候我们发现报错了编译器不认识pthread_create函数。
我们创建新线程只能通过原生线程库去创建此时编译器找不到原生线程库。
我们要使用-l选项指定原生线程库pthread之前在动态静态库的时候详细介绍过如果指定动态库。
Makefile
mythread:mythread.ccg -o $ $^ -stdc11 -lpthread
.PHONY:clean
clean:rm -f mythread
再次编译运行上面代码 此时就证明pid是一样的再看下创建几个新线程的代码
#include iostream
#include unistd.h
#include pthread.husing namespace std;void* threadRun(void *args)
{const string name (char *)args;while (true){cout name , pid: getpid() endl;sleep(1);}
}int main()
{pthread_t tid[5];char name[64];for (int i 0; i 5; i){snprintf(name, sizeof(name), %s-%d, thread, i); // 特定内容格式化到name中pthread_create(tid i, nullptr, threadRun, (void*)name);sleep(1); // 缓解传参的bug}while (true){cout main thread, pid: getpid() endl;sleep(3);}return 0;
}
编译运行 新线程和主线程都在同时运行并没有陷入某一个死循环中。 2.4 原生线程库LWP和PID
再前面基础上ldd pthread查看链接属性再ll /lib64/libpthread.*查看原生线程库 查看可执行程序的链接属性。可以看到是动态链接链接的库是原生线程库如上图绿色框中所示。
根据线程库的路径去查看该路径下的所有文件可以看到还有静态库我们使用的线程库是一个软链接文件它所链接的库才是真正的原生线程库。 在前面程序运行的时候打开一个窗口输入ps ajx | head -1 ps ajx | grep mythread 主线程和新线程在同时运行此时存在两个执行流。 但是在查看该进程的时候发现mythread进程只有一个pidppid等值也只有一个。 这也证明线程是进程中的一个执行流线程属于进程的一部分。 给线程发现kill -9 信号 给mythread进程发送9号信号主线程和新线程都结束了。 所有信号针对的都是进程而线程属于进程。当一个进程结束以后它的所有资源都会被回收所以线程也就不存在了。 那我们想看到线程该怎么办呢
再运行程序使用指令ps -aL来查看线程。L必须大写输入ps | aL 此时名字为mythread的线程有6个它们的PID值相同LWP不同。
PID进程标识符LWP轻量级进程表示符LWP是英文Light Weight Process的首字母。 可以看到第一个线程的LWP和PID是一样的这个线程就被叫做主线程。 其它线程的LWP和PID不一样这些线程就被叫做新线程。 那么CPU在调度PCB的时候根据的是LWP呢还是PID呢
CPU在调度PCB的时候是根据LWP为标识符表示一个特点的执行流的。
因为CPU调度的都是轻量级进程而每个轻量级进程也就线程的根本区别就在于LWP不同但是不同线程的PID却有可能相同。
我们之前学习的进程它只有一个执行流也就是主线程所以它的PID和LWP是相同的即PID LWP我们使用哪个都无所谓。而现在我们学习了线程就不能再只使用PID了而是使用LWP。 3. 线程的公有资源和私有资源
3.1 线程的公有资源
所有线程都共享一个虚拟地址空间一个页表所以进程中的绝大部分资源都是所有线程共享的先来看看共享的情况写了一个公有函数分别在主线程和新线程中调用这个函数
#include iostream
#include unistd.h
#include pthread.h
using namespace std;void show()
{cout show , pid: getpid() endl;
}void* threadRun(void *args)
{const string name (char *)args;while (true){cout name , pid: getpid() endl;show();sleep(1);}
}int main()
{// pthread_t tid[5];// char name[64];// for (int i 0; i 5; i)// {// snprintf(name, sizeof(name), %s-%d, thread, i); // 特定内容格式化到name中// pthread_create(tid i, nullptr, threadRun, (void*)name);// sleep(1); // 缓解传参的bug// }pthread_t tid;pthread_create(tid, nullptr, threadRun, (void*)thread 1);while (true){cout main thread, pid: getpid() endl;show();sleep(1);}return 0;
} 可以看到主线程和新线程都可以调用这个函数。
该进程中只有一份虚拟地址空间该函数放在代码段中。所有线程共享一个代码段。
创建一个全局变量在主线程和新线程中都打印并且使用后置。
将全局变量的地址在主线程和新线程中打印出来。
#include iostream
#include unistd.h
#include pthread.h
using namespace std;int g_val 100;void show()
{cout show , pid: getpid() g_val: g_val g_val: g_val endl;
}void* threadRun(void *args)
{const string name (char *)args;while (true){cout name , pid: getpid() g_val: g_val g_val: g_val endl;show();sleep(1);}
}int main()
{// pthread_t tid[5];// char name[64];// for (int i 0; i 5; i)// {// snprintf(name, sizeof(name), %s-%d, thread, i); // 特定内容格式化到name中// pthread_create(tid i, nullptr, threadRun, (void*)name);// sleep(1); // 缓解传参的bug// }pthread_t tid;pthread_create(tid, nullptr, threadRun, (void*)thread 1);while (true){cout main thread, pid: getpid() g_val: g_val g_val: g_val endl;show();sleep(1);}return 0;
} 新线程和主线程看到的全局变量是一个当任意一个线程改变这个变量的值时都会影响另一个线程使用这个值。主线程和新线程中全局变量的地址是相同的说明它们使用的是同一个全局变量。
根据上面现象以及分析可以知道数据段也是被所有线程共享的。
进程中的绝大部分资源都是和所有线程共享的。 3.2 线程的私有资源
因为所有线程都共享一个虚拟地址空间以及页表线程之间有私有资源吗答案肯定是有的。
PCB属性私有
所有线程都有各自的PCB所以PCB中的属性肯定是私有的属于各自线程。 上下文数据私有
CPU在调度PCB的时候采用轮转时间片的方式当一个线程被换下时该线程的上下文一定是私有的防止被其他线程修改而导致恢复上下文的时候出现错误。 栈结构私有
不同线程各自的临时变量一定是私有的而临时变量存放在栈结构中所有栈也是私有的。
都是同一块虚拟地址空间怎么就让不同线程的栈结构私有了呢这就涉及到了原生线程库的实现man clone 系统调用clone是用来创建子进程的这里的子进程是轻量级进程也就是没有独立的虚拟地址空间。
clone中有一个参数void* child_stack该参数就是用来自定这个子进程的栈空间的。
所以我们在使用pthread_create创建新线程的时候底层会调用clone并且会指定属于该线程的私有栈结构。 4. 线程的优缺点
4.1 线程的优点
与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多。
进程切换PCB切换 上下文切换 虚拟地址空间切换 页表切换线程切换PCB切换 上下文切换
可以看到线程切换比进程切换少了两项。 除此之外线程切换时硬件缓冲区不用太多地更新。 cache硬件缓冲区其实就是我们所说的高速缓存。 它存在于CPU中速度只是比CPU慢一点但是比内存快很多。 cache会根据局部性原理从内存中拿数据尤其是使用频率高的热点数据会一直放在cache中。CPU在使用数据时不是直接去内存中拿而是先去cache中拿如果不命中也就是不存在cache就会将CPU所需要的数据从内存中缓存到cache中。 进程间切换不仅上面提到的四项内容需要切换而且cache中的内容也需要重新缓存。 线程间却换切换PCB和上下文但是cache中缓存的数据不需要切换。
所以线程都共用一个虚拟地址空间和一个页表而cache中的内容也是根据虚拟地址和页表缓存进来的所以不同进程之间是可以共用的。
这样一来大大节省了cache从内存中缓存数据的时间并且也节省了操作系统的大量工作。
当然还有很多其他的优点比如
创建一个新线程的代价要比创建一个新进程小得多因线程不创建新的虚拟地址空间和页表。线程占用的资源要比进程少很多。能充分利用多处理器的可并行数量。在等待慢速I/O操作结束的同时程序可执行其他的计算任务。计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现。I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。 计算密集型应用主要体现在CPU的高频工作如加密解密算法等。 I/O密集型应用主要体现在和外设的交互上如访问磁盘显示器网卡等。 上面很多线程的优点进程也是拥有的。 4.2 线程的缺点
健壮性或者鲁棒性较差
编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。
还有比如一个线程里出现了异常退出其它进程也会退出
#include iostream
#include unistd.h
#include pthread.h
using namespace std;int g_val 100;void show()
{cout show , pid: getpid() g_val: g_val g_val: g_val endl;
}void *threadRun(void *args)
{const string name (char *)args;while (true){cout name , pid: getpid() g_val: g_val g_val: g_val endl;show();sleep(1);static int cnt 0;if (cnt 7){int *p nullptr;*p 777;}cnt;}
}int main()
{pthread_t tid[5];char name[64];for (int i 0; i 5; i){snprintf(name, sizeof(name), %s-%d, thread, i); // 特定内容格式化到name中pthread_create(tid i, nullptr, threadRun, (void *)name);sleep(1); // 缓解传参的bug}// pthread_t tid;// pthread_create(tid, nullptr, threadRun, (void*)thread 1);while (true){cout main thread, pid: getpid() g_val: g_val g_val: g_val endl;show();sleep(1);}return 0;
}
新线程中发送端错误异常收到了11号信号SIGSEGV。
但是不仅这个新线程结束了主线程和其它线程也结束了。 线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出。而多进程就不存在一个进程的退出并不会影响另一个进程。 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销(线程切换)而可用的资源不变。 缺乏访问控制
进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多、 一般情况下CPU有几个核就创建几个线程。 核只的是一个CPU中的运算器个数即使是多核而控制器也是一个CPU只有一个。 本篇完。
下一篇零基础Linux_22多线程线程控制和和C的多线程和笔试选择题。