Linux解锁线程基本概念和线程控制,步入多线程学习的大门(1)
1、线程初识
1.1线程的概念
线程是进程内部的一个执行分支,线程是CPU调度的基本单位
那什么是进程呢?
我们之前学习了解到的进程是加载到内存中的程序
进程 = 内核数据结构 + 进程代码和数据。
今天我们要推翻这一观点,该观点是片面的,不正确的!
我们之前认为的task_struct就是进程,其实这一个个的task_struct是我们的进程的执行流!!!那进程究竟是什么呢?
进程其实是包含文件描述符表,pcb,页表等等,上面框的一整套才是真正的进程!
1.2.关于线程和进程的进一步理解
我们要抛弃之前的想法,进程不仅仅是PCB数据结构。而是一整套资源的时候,我们就应该清楚进程创建成本很高。原因就是:创建进程时还需要构建文件描述符,信号表,PCB,页表,等等这就会造成空间和时间的浪费。
从内核来看:进程本质上是是一个容器,承担分配系统资源的基本实体,包括执行流资源、地址空间资源、页表映射关系,代码和数据这一整套的资源。
线程只是进程当中的一个执行分支~
1.3.线程的设计理念
线程我们一般称为tcb(进程是pcb),对于线程来说,也一定要和进程一样需要对应操作方法:新建,暂停 ,销毁,调度。那我们如何对线程进行这些操作呢?
如果我们要设计线程,OS也要对线程进行管理。(先描述,再组织)
Linux的设计者认为,进程和线程都是执行流,具有极度的相似性,没必要单独设计数据结构和算法,直接复用代码,所以Linux是用进程模拟的线程!
1.4.进程vs线程(图解)
横向的是进程,纵向的是执行流。我们以前讲的进程,是今天讲的特殊情况
1.5地址空间的第四谈
如何从页表定位到物理地址?
页目录保存的是二级页表的地址
以后我们在查页表的时候,先拿虚拟地址中的前10位,查页目录,选择具体哪一个页表,然后再查询虚拟地址的中间10位就能找到页框,就肯定能找到一个物理地址,最后我们拿着虚拟地址的最后12位,就是页内偏移,因此就直接找到物理内存中的地址了,有页框的物理初始地址,后面还有虚拟地址后12位的页内偏移就可以定位一个物理地址了!
页表里面存储的是页框的物理地址。
给不同的线程分配不同的区域,本质就是让不同的线程,各自看到全部页表的子集!
2.线程的控制:
2.1.关于线程控制的前置知识
在学习之前,我们要清楚关于创建新线程的前置知识——POSIX线程库。
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文<pthread.h>
链接这些线程函数库时要使用编译器命令的“-lpthread”选项(makefile文件要加上-lpthread”选项)
用户层面:用户知道“轻量级进程”这个概念吗?并没有,只有线程和进程,
系统层面:将轻量级进程的系统调用进行封装,转成线程相关的接口提供给用户
内核层面:Linux到底有没有真线程呢?没有,Linux只有轻量级进程。Linux系统,不会有线程相关的系统调用,只有轻量级进程的系统调用
新线程和主线程,谁先运行呢?不确定,由调度器决定
2.2创建线程的系统调用:
这个几号手册具体代表的什么含义?
这个3就是代表他在3号手册中
这种几号手册,一般来说,1号手册都是一些命令,2号手册都是系统调用,3号手册就是C库函数。就是分门别类放置不同的东西。
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
代码:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
// 新线程
void *threadStart(void *args)
{
while (true)
{
std::cout << "new thread running..." << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadStart, (void *)"sthread-ew");
// 主线程
while (true)
{
sleep(1);
std::cout << "main thread running..." << std::endl;
}
return 0;
}
当我们进行make编译的时候,会报错:(.text+0x1b): undefined reference to main线程未定义,之所以会出错是因为Linux下使用线程需要引用线程库。必须在makefile文件中加上-lpthread,因为线程库里面是函数。编译器并不认识,不是C语言/C++自带的,而是Linux自己创建的原生线程库文件
再加上线程库之后,就可以正常运行了。
可以看见,主线程和新线程是可以同时运行的!并且主线程和新线程用的是同一个pid。
所以这两个虽然都是不同的执行流,但是是属于同一个进程内部的,我们可以使用ps -al来查看不同的线程信息
这个pid是对应进程的pid,这个LWP其实就是这个线程的id!!!
2.3.线程终止
同一个进程内的线程,大部分的资源都是共享的,地址空间是共享的!
主线程退出 == 进程退出 == 所有线程都要退出
多线程代码往往健壮性不好。进程之间是独立的,不能共享资源哦。但是线程可以共享资源哦!
我们怎么没有像进程一样获取线程退出的退出信号呢?
因为线程出异常了,会将整个进程退出,根本没有机会读到pthread_read的退出信号,因此该函数并不考虑退出信息。但是父进程会接受子进程退出时的异常信息
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
线程可以调用pthread_ exit终止自己。
一个线程可以调用pthread_ cancel终止同一进程中的另一个线程
不能用exit终止线程,因为他是终止进程的!
pthread_ exit函数
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
pthread_ cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码