目录
前言:
Linux的学习从开始到现在,我们已经经历了许多大boss,从一开始的熟悉指令,到第一次在gcc环境下编译C语言的代码,到理解文件系统,比如理解了文件的权限,万物皆文件的概念,此时,是我们经历的第一次大boss,文件系统。
之后,我们从shell开始慢慢理解Linux的系统内核部分,最典型的是我们慢慢开始理解了什么是进程,从pcb->task_struct到mm_struct地址空间,到页表部分,最后我们从物理内存出发,一步一步的理解了程序的地址,之后,我们学习了进程的状态,学习了进程的控制,学习了进程等待,学习了进程终止,以及进程的各种信息,比如Pid,这是第二次大boss。
今天,我们学习的是Linux中的第3个大boss,线程。线程我们同样,从概念入手,再到线程的控制,线程同步,线程互斥等,和前两个一样,都是需要我们反反复复学习的知识点。
那么,话不多说,本文作为线程的概念篇,主要是解释线程中的概念,并且结合少许的代码。
进入第一个主题吧!线程的概念。
线程的背景
介绍线程概念我们打算从地址空间入手,在地址空间篇章,我们就提及到过,地址空间我们是要多次介绍的,一共要介绍4次左右,今天就是第四次。
我们知道,之前对于地址空间的理解就是:
这个图我们已经十分熟络了,通过页表和MMU可以将虚拟内存和物理内存建立某种联系。可是,今天我们对于页表要理解的更深层一点。
假设在磁盘中有一个hello.o,它是文件吧?那么既然他是文件,它就应该有自己的inode,它也应该存在于物理内存中。既然是和物理内存挂钩,在前文介绍的磁盘管理中,分为扇区等区域,并且是以4kb的大小进行管理。此时,真实的数据块加载到了物理内存里面,通过某种方式,和虚拟内存建立了联系。
那么,我们不妨来解析一下虚拟内存,我们将虚拟内存分为三部分:0000 0000 00,前10位是第一部分,00 0000 0000,中间10位,是第二部分,0000 0000 0000是第三部分。
好吧,在此之前我们其实还是应该再深度解析一下物理内存。
磁盘中的文件的数据是以4kb的方式存在的,我们将每个物理内存叫做页框,每个文件的数据块叫做页帧,其实现在操作系统的书已经没有区分的那么多了。
对于IO来说,IO的基本单位是4kb,OS要进行内存管理的话,不是以字节的方式管理的,而是以内存块的方式管理的,所以文件的数据块,物理内存的页框都是内存管理的对象。
那么OS应该如何管理页框页帧呢?
当然是先描述再组织。在Linux中有一种结构体是struct page,里面存的就是页框的信息。
在Linux的源码里应该如何管理呢?已经描述好了,那么使用一个数组就管理起来了。
现在我们在来刨析虚拟内存,前10个比特位,其实是指向的页目录,我们是拿32位机器举例,所以页目录实际上是由1024个,那么一个一个的页目录,指向了一个一个的页表,对于虚拟地址的中间10位,指向的就是页表,对于后12位的虚拟地址,就是先定位到了页目录之后,才好定位到页框的位置,这样,就能通过虚拟地址找到物理内存了。
那么,什么是函数呢?或者说,虚拟地址的本质是什么呢?
其实在mm_struct中,正文的代码本来就是由一个一个的地址构成的,对于函数来说,不过是一连串的地址而已。所以对于代码数据划分的本质,不过是一种对于资源的划分!
线程的概念和Linux中的线程实现
上面其实是对于页表的一种重新理解,可能有人觉得和今天的主题线程没有关系,实则不然,因为今天实际上会对之前进程的理解有一个颠覆性的理解。
在深入了解操作系统这本书里面说的好,进程是一种抽象。我们之前认为的进程是task_struct + mm_struct + 页表集合等。认为进程 = 内核数据结构 + 自己的代码和数据。
可是,在内核的观点里面,认为进程实际上是承担分配系统资源的基本实体。
今天就不讲故事了,我们直接说,以前认为task_struct就是进程,认为cpu调度的时候就是调度的task_strcut。
实际上不是的,一个进程可以存在多个task_struct,而对于task_struct就是Linux中的线程,为什么说是Linux中的线程呢?因为对于windows来说,windows也有自己的线程标准。
我们这样理解吧,对于国家来说,分配资源的实体是一个一个的家庭,家庭中的许多成员就是一个一个的线程。
那么windows对于线程是提供了真实线程控制块,对于Linux来说,是直接复用的内核代码,不然单独创建线程控制块,增加管理成本,源码还要多写很多很多行。
所以Linux中的线程实际上是集成在进程里面的。所以之前理解的进程实际上是只有一个线程的进程,我们之后要学习的就是一个进程含有多个线程。
那么对于cpu来说,是否要区分什么是线程,什么是进程呢?因为task_strcut理解存放的是进程的信息,但是实际上进程执行任务的时候使用的是线程。
cpu是不用区分的,因为cpu看到的执行流<=进程。我们将Linux中的线程成为轻量级进程。
那么既然有了多进程,为什么还要多线程呢?
因为cpu内部调度的时候,时间片一到,进程切换需要存上下文吧?地址空间,页表全部都要切换吧?那么这个成本是不是十分高了就?如果使用的是多线程,线程之间共享的是地址空间,页表,切换的时候成本就很低了。所以这是多线程的优势。
但是就和家庭一样,一个线程如果奔溃了,其他线程也都是会崩溃的。
线程杂谈
说了那么多,我们总的看看吧?
使用函数我们可以创建线程,其实第一个参数不解释了,第二个参数我们设置为nullptr即可,对于第三个参数就是函数指针,信号那里我们也见过,第四个参数是线程的名字。
我们直接来一份代码:
int gval = 100;
void *threadStart(void *args)
{
while (true)
{
sleep(1);
std::cout << "new thread running..." << ", pid: " << getpid() << std::endl;
// std::cout << "new thread running..." << ", pid: " << getpid()
// << ", gval: " << gval << ", &gval: " << &gval << std::endl;
}
}
int main()
{
srand(time(nullptr));
pthread_t tid1;
pthread_create(&tid1, nullptr, threadStart, (void *)"thread-new");
// pthread_t tid2;
// pthread_create(&tid2, nullptr, threadStart, (void *)"thread-new");
// pthread_t tid3;
// pthread_create(&tid3, nullptr, threadStart, (void *)"thread-new");
// 主线程
while (true)
{
std::cout << "main thread running..." << ", pid: " << getpid() << std::endl;
// gval++;
sleep(1);
}
return 0;
}
按照平常,我们直接g++是编译不过去的,因为它需要链接库,没想到到,重制版介绍为什么。
所以需要在Makefile文件里面加-lpthread
按照常理来说,两个死循环是不会同时打印的,可是一个进程使用两个线程调用不同的任务,就可以同时打印了:
像这样。
那么我们定义一个全局变量,看看g_val的变化:
发现线程是共享数据的,也就是地址空间都是同一份,这个我们也成功验证了。
当我们输入ps -aL指令,我们能查看线程的部分信息,可以发现和进程Pid相等的线程实际上是主线程,其他的是非主线程了,那么提问了,对于cpu来说,调度的时候看的是pid还是lwp呢?
当然是lwp了,毕竟是线程来执行的任务。
这里提及一个非常重要的点:
线程虽然共享了许多资源,但是线程私有的部分有一组寄存器,栈。前者要存储硬件的上下文,后者是线程运行的时候会形成各种临时变量,肯定都会被自己的线程保存在自己的栈区里。
本文不过是对线程的非常粗略的概念理解,请各位佬不要介意没有解释清楚大部分知识,重制版一定非常清晰的介绍清楚了。
感谢阅读!