线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位
在操作系统中,线程是实现并发编程和提高系统性能的重要手段,广泛应用于各种类型的程序开发中
这里我们举一个简单的例子帮助大家理解,我们都知道,盖一栋大楼需要土木的同学画图纸,建筑工人打地基,吊车工人运送原料,设计的同学设计室内,货车司机运水泥等等,他们共同在做的就是盖大楼,但他们每个人都在做自己的事情,这些事情是无法分开的,但合起来就是盖楼的一个过程,这就是我们进程和线程之间的关系,每个人做的工作都是线程,所有人做的工作合起来就是进程
我们以前说的进程就是单线程进程,开个玩笑说一个人把楼给盖起来的
所以我们要理解的是,进程和线程真正的概念,进程是承担分配系统资源的基本实体,线程是我们进程内部的执行流资源,简单来说由进程来帮我们申请空间分配资源,线程在进程分配的资源中进行线程的执行
线程作为一个轻量级的进程,在CPU中也是要进行切换的,一个进程中的线程共享这个进程的时间片,描述线程的数据结构也是task_struct
结构,当然其中有标识它是线程的标识符id
,在我们CPU执行进程的时候,会将缓存数据加载到寄存器cache
中,在线程进行切换的时候,由于共享数据的特性,cache
中的数据不用进行更换,效率高,时间片结束后,进程切换时,才进行cache
数据更换
今天我们借线程这个话题再谈进程地址空间,线程是进程的细分,这个细分也要由页表找到物理内存,它是怎么找到的呢,这里我们就不得不讲解一下页表的实现结构了
实际上,页表并不像我们前面说的一列是虚拟内存地址,另一列是物理内存地址,它们一一对应,这只是我们抽象出来的,忽略了页表自身结构,而保留它的功能的一个抽象的概念,有一个很简单的问题,如果页表的实现是这样的话,以32位
计算机为例,它的一个地址就需要页表中4个字节
来存储,而我们的虚拟地址中有页的概念,一页4KB
,虚拟内存一共有多少页那页表就有多少虚拟地址,已知虚拟内存共4GB
,也就是4*1024*1024÷4
个地址,那光虚拟内存就要4*1024*1024bytes = 4MB
连续的物理内存,并且大部分进程实际使用的虚拟地址空间只是其中一小部分,这就意味着页表中大量的页表项是无效的,却依然占用着物理内存,造成了极大的浪费,所以我们就要引出真正的页表结构了
我们页表的结构就是多级页表,上面所说的这种页表结构叫做一级页表,这样的页表我们也看到了,会占用大量连续的物理内存,并且很多情况下大部分页表项可能都是无效的,造成内存浪费,是不可行的,多级页表结构通过将页表分级,仅在需要时才分配和使用各级页表,从而有效减少内存占用
虚拟地址需要32个比特位来存储,我们将这32个比特位分开存储,前十个,中间十个,最后十二个共三个部分,第一部分叫做外层页表,第二部分叫做内层页表,第三部分叫做页内偏移
前十个比特位叫做外层页表(页目录),页目录中的每个条目都指向相对应的一个内层页表,把前十位看做一个十进制数字,这个数字的大小就是页目录的下标,页目录占用空间的大小为2^10*4 bytes = 4KB
,页目录的条目数和内层页表的条目数相同,是一一对应的
中间十个比特位叫做内层页表(二级页表),二级页表中的每个条目都指向物理地址空间的一个页,把中间十位看做一个十进制数字,这个数字的大小,就是该地址在物理地址空间的页码下标,一个二级页表的大小也是4KB
,但是二级页表一般都是不全的,申请才有,不申请就没有
最后十二个比特位叫做页内偏移(偏移量),前面已经指向了物理地址空间的一个页了,我们知道页的大小就是4KB
,也就是2^12字节
,而最后剩下十二个比特位正是用来形容它的,我们知道每个字节都有一个地址,这里页内偏移就是地址相对于这个页起始地址的偏移量
pthread_create
用于创建一个新线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
返回值:成功返回0,失败返回非零错误码
thread
:这是一个指向 pthread_t
类型变量的指针,pthread_t
是一个线程标识符类型,用于唯一标识一个线程,当 pthread_create
成功创建一个新线程时,会将该线程的标识符存储在 *thread
所指向的内存位置
attr
:这是一个指向 pthread_attr_t
类型的常量指针,用于指定新线程的属性,如果将其设置为 NULL,则表示使用默认的线程属性,pthread_attr_t
类型定义了一系列线程的属性,如线程的栈大小、调度策略、分离状态等。可以使用 pthread_attr_init
函数初始化一个 pthread_attr_t
对象,并使用其他相关函数来设置具体的属性,但是我们一般用不到这些属性,我们线程用来就是实现多任务调度的
start_routine
:这是一个函数指针,指向新线程开始执行时要调用的函数,该函数必须接受一个 void *
类型的参数,并返回一个 void *
类型的值,新线程从这个函数开始执行,直到该函数返回或者线程被取消
arg
:这是传递给 start_routine
函数的参数,由于 start_routine
函数的参数类型是 void*
,因此可以将任意类型的数据指针转换为 void*
类型传递给该函数,在 start_routine
函数内部,需要将其转换回原来的类型,这个我们下一篇文章再谈,这里就简单使用一下
下面是一个简单的测试线程创建的代码主线程和新线程的任务都是循环打印自己的pid
#include <iostream>
#include <pthread.h>
#include <unistd.h>
void *threadRun(void* args)
{
while(1)
{
std::cout << "new thread: " << getpid() << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, nullptr);
while(1)
{
std::cout << "main thread: " << getpid() << std::endl;
sleep(1);
}
}
注意这里的makefile
文件,我们将所对应的库写上了,这个库叫做POSIX线程库,它并不是操作系统原生自带,但在Linux系统中都会有这个库,因为Linux内核提供的clone()
函数调用更加复杂,clone
是Linux内核用来创建轻量级进程的函数,我们的pthread_create()
就是基于它封装的,使用方便
简单看一下clone函数,确实是要比pthread_create()
要难用的
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
我们查看打印结果,虽然因为进程调度的原因偶尔会出现重叠乱打的情况,但是大部分时间还是正常的,从这个乱打的情况下我们可以发现,线程之间是不存在同步和互斥的概念的,我们还可以通过打印结果发现一个特点,它们的进程pid是相同的,即一个进程中的多个线程共享一个进程pid
在进程执行的过程中,我们可以通过命令ps -aL
查看线程资源,我们线程也有自己的唯一标识符LWP
表示light weight process
也就是轻量级进程,就是线程,我们发现两个线程的PID相同这我们上面说了,其中一个线程LWP
与PID
相同,这个线程就是主线程
今日分享就到这了~