- 线程是一个执行分支,执行力度比进程更细,调度成本更低
- 线程是 进程内部的一个执行流
概念理解:
调度成本更低,指的是,相较于进程,线程不再需要进行对 cache(高速缓存)
进程是承担分配系统资源的基本实体:进程是一系列资源的集合,包括至少一个 task_struct(执行流)、包括虚拟地址空间、页表、自己的代码和数据等等…内容。
把进程从磁盘加载到内存,也就是加载可执行程序形成进程,可以换一个说法了:把该可执行程序,加载到内存,让 OS 为该进程申请与该进程匹配的所有资源。
之前提到的进程可以理解为:内部只有一个 task_struct(执行流) 的进程。
操作系统是需要对这么多线程管理的,有些操作系统设计了 TCB (thread control block,线程控制块,属于进程 PCB),调度进程、线程都有各自的调度方法。Windows 就是这样设计的。
Linux 内核的设计:复用 PCB 的结构体,用 PCB 模拟线程的 TCB。也就是说,Linux 没有真正意义上的线程,而是用进程方案模拟的线程。
复用代码和结构,使得 Linux 系统更简单,好维护效率更高,也更安全。这也是 Linux 可以不间断的运行的原因,因为一款 OS 操作系统,使用最频繁的功能,除了 OS 本身,接下来就是进程了
每个 pcb 叫做 轻量级进程(light weight process),查询这些线程时,出现的 LWP 就叫做 轻量级进程id。
PID 和 LWP 相等,就说明是主线程。操作系统调度的时候,其实不是用 PID 识别进程,而 是用 LWP 进行识别的。
优点:
ps:对于计算密集型应用,线程不是越多越好,进程 和 线程,与 CPU 的个数 / 核数 一致是比较合适的。
pps:对于 I/O 密集型应用,自然也不是越多越好,但是可以比较多,这个“比较”无法量化,需要结合具体场景进行试验来确定。
缺点:
OS 在和磁盘这样的设备进行 IO 交互的时候,绝对不是按照字节为单位的,而是按照 块 为单位。而磁盘这样的外设工作速度是很慢的,页框 和 页帧 的设计,包括 局部性原理,可以大大提高 IO 的效率
这里的块 我们一般做 4kb 考虑,这是文件系统的要求,具体和 OS 有关。
可以说内存管理的本质,是决定将磁盘中特定的 4kb 块(数据内容)放入到哪一个物理内存的 4kb 空间(数据保存的空间)。
首先 文件系统 + 编译器:注定了文件(可执行程序 + 动态库)在磁盘的时候就是以块(4kb)为单位的。
其次 操作系统 + 内存:内存实际在进程内存管理的时候,也要以 4kb 为单位。
这样 物理内存 中的每 4kb 称作 页page,也叫页框
磁盘 中的每 4kb 称作 页帧
这其中的 页page,会有一个个 struct page{};
进行状态标识等,里面属性非常少。这些 page 结构体,会由类似 struct page mem[1,048,576]
这样的数组进行管理,刚好数组下标可以访问到物理内存的每一个页框。
局部性原理:OS 会提前加载正在访问的数据相邻或者附近的数据。我们通过预先加载要访问的数据的附近的数据,来减少未来 IO 的次数,多加载数据进来的本质,就叫做数据的预加载
页表储存的核心功能是映射 虚拟地址 和 物理内存 的位置关系,要了解页表的设置方法,首先要知道:虚拟地址不是整体被使用的,而是按照
10 + 10 + 12
比特进行划分的。
页表:页目录 + 页表项,(32 位系统为二级页表方案,64 位系统为三级页表)具体如图示。
虚拟地址的最高 10 位,以供寻找页目录对应的页表项
虚拟地址的次高 10 位,供页表项找到对应的物理内存的页框
虚拟地址的低 12 位,访问一个块的 4kb 中的具体哪一个字节
虚拟地址对应的物理内存的定位 = 对应页框的起始地址 + 虚拟地址的低 12 个比特位对应的地址数据
即:定位任意一个内训字节位置 = 页框 + 页内偏移
= 基地址 + 偏移量
2^12:0000 0000 0000 ~ 1111 1111 1111
[0, 4095]
估算页表的可预见大小:
2^20 = 1MB
1MB * 4 = 4MB
但是页表不会全部创建,就注定了页表的实际大小远远小于 4MB。
我们在实际 malloc 申请内存的时候,OS 只需要在虚拟地址空间上申请就行了,当我们真正访问的时候,OS 查到没有相应页表,向寄存器 MMU 发送缺页中断,OS 接收到这一动作,就会自动去申请或填充页表,并申请具体的物理内存,接着再继续跑我们后面的代码。
我们知道上述字符常量区的数据是只允许读取,不允许修改的。
char *s = "hello world";
*s = 'H'; // err...
原因是,s 里面保存的是指向字符的虚拟起始地址,对其寻址的时候,必定会伴随虚拟到物理的转化(MMU + 查页表的方式),如果对这个操作进行权限审查,就会发现我们只有读权限即写操作是非法的。此时 MMU 发生异常,OS 识别异常,异常转化成信号发送给目标进程,进程在从内核转化成用户态的时候,进行信号处理,收到了终止进程的信号,至此进程被终止。
# 查看文件执行流
ps -aL | grep [可执行程序]
# 带上表头,查看文件执行流
ps -aL | head -l && ps -aL | grep [可执行程序]
#include
注意:使用本章函数,需要在编译时,声明库,即带上 -lpthread
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数 thread:
- 创建线程的地址
参数 attr:
- 线程属性,一般设成 nullptr
参数 start_routine:
- 线程 thread 所要执行的函数
参数 arg:
- 作为 strat_routine 回调函数的参数,可以传对象、字符串等等
返回值:
- 成功返回 0,失败返回错误码。
#include
int pthread_join(pthread_t thread, void **retval);
参数 thread:
- 等待的线程
输出型参数 retval:
- 给自己创建的 void* retval 取地址。拿到等待线程的相关结果
返回值:
- 成功返回 0,失败返回错误码。
#include
void pthread_exit(void *retval);
输出型参数 retval:
- 拿到等待线程的相关结果
#include
int pthread_cancel(pthread_t thread);
参数 thread:
- 线程名
主线程使用函数,新线程取消导致的退出,失败返回错误码为 -1( PTHREAD_CANCELED),pthread_join 可以拿到。
#define PTHREAD_CANCELED (void *)-1
#include
pthread_t pthread_self(void);
返回值:
- 调用这个函数的线程的线程 id
一个线程如果被分离,就无法再被 join,如果 join,函数会报错
#include
int pthread_detach(pthread_t thread);
参数 thread:
- 需要被分离的线程名
返回值:
- 成功返回 0,失败返回错误码
使用举例 1:
#include
#include
#include
#include
using namespace std;
void *threadRun(void* args)
{
const char*name = static_cast<const char *>(args);
int cnt = 5;
while(cnt)
{
// 打印线程名称
cout << name << " is running: " << cnt-- << " obtain self id: " << pthread_self() << endl;
sleep(1);
}
// 线程正常退出
pthread_exit((void*)2);
// 线程被取消会将 -1 传给参数 ret
// PTHREAD_CANCELED; #define PTHREAD_CANCELED ((void *) -1)
}
int main()
{
// 1. 创建线程
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
// 2. 取消线程
sleep(3);
pthread_cancel(tid);
// 3. 等待线程
void *ret = nullptr;
pthread_join(tid, &ret);
cout << " new thread exit : " << (int64_t)ret << "; quit thread: " << tid << endl;
return 0;
}
输出结果:
使用举例 2:创建多个线程,每个线程执行 [1, top] 的求和计算
#include
#include
#include
#include
#include
using namespace std;
#define NUM 10
enum{ OK=0, ERROR };
class ThreadData
{
public:
ThreadData(const string &name, int id, time_t createTime, int top)
:_name(name),
_id(id),
_createTime((uint64_t)createTime),
_status(OK),
_top(top),
_result(0)
{}
~ThreadData()
{}
public:
// 输入的
string _name;
int _id;
uint64_t _createTime;
// 返回的
int _status;
int _top;
int _result;
};
// 线程终止
// 1. 线程函数执行完毕,return void*
// 2. pthread_exit(void*)
// 3. pthread_cancel(int id)
void *thread_run(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // static_case<>:类型强转,和括号强转一个意思,但是更安全
for(int i = 1; i <= td->_top; i++)
{
td->_result += i;
}
cout << td->_name << " cal done!" << endl;
pthread_exit(td);
//return td;
}
int main()
{
// 线程创建
pthread_t tids[NUM];
for(int i = 0; i < NUM ;i++)
{
char tname[64];
snprintf(tname, 64, "thread-%d", i+1);
ThreadData *td = new ThreadData(tname, i+1, time(nullptr), 100+5*i);
pthread_create(tids+i, nullptr, thread_run, td);
sleep(1);
}
// 线程等待(并获取新线程退出信息)
void *ret = nullptr; // int a = 10
for(int i = 0 ; i< NUM; i++)
{
int n = pthread_join(tids[i], &ret);
if(n != 0) cerr << "pthread_join error" << endl;
ThreadData *td = static_cast<ThreadData *>(ret);
if(td->_status == OK)
{
cout << td->_name << " 计算的结果是: " << td->_result << " (它要计算的是[1, " << td->_top << "])" <<endl;
}
delete td;
}
cout << "all thread quit..." << endl;
return 0;
while (true)
{
cout << "main thread running, new thread id : " << endl;
sleep(1);
}
}
上述已经涉及到的接口,是原生系统库的系统级解决方案,虽然在库中实现但是跟语言一样,比语言更靠近底层罢了。而 C++ 其实是对上述线程库做的封装,所以在 Linux 编译使用时还得标记 -lpthread
库文件。
使用举例:
void run1(){}
void run2(){}
int main()
{
// 创建线程
thread t1(run1);
thread t2(run2);
// 等待线程
t1.join();
t2.join();
return 0
}
ps:虽然原生接口效率更高,但是 语言接口 有跨平台性。不确定只在 Linux 下跑的还是推荐使用语言接口。
使用举例:
makefile:创建线程,需要在编译的时候导入线程库,添加 -lpthread
threadTest:thread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f threadTest
#include
#include
#include
#include
#include
using namespace std;
void *threadRun(void* args)
{
const char*name = static_cast<const char *>(args);
int cnt = 5;
while(cnt)
{
cout << name << " is running: " << cnt-- << " obtain self id: " << pthread_self() << endl;
sleep(1);
}
pthread_exit((void*)11);
// PTHREAD_CANCELED; #define PTHREAD_CANCELED ((void *) -1)
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
// sleep(3);
// pthread_cancel(tid);
void *ret = nullptr;
pthread_join(tid, &ret);
cout << " new thread exit : " << (int64_t)ret << "quit thread: " << tid << endl;
return 0;
}
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
ps:关于各自所有寄存器和栈的深度理解,见 线程篇Ⅱ
进程的多个线程共享 同一地址空间,因此 Text Segment、Data Segment 都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL 或者自定义的信号处理函数)
当前工作目录
用户id和组id
线程篇Ⅱ:线程库和线程id、线程的互斥与同步