文章目录
attribute 属性 argument 参数 start_routine 常规 来自 pthread_create
在一个程序中一个执行线路称作线程(thread)。更为确切的定义是:线程是“一个进程内部的控制序列”,一个进程内部可以拥有多个线程。但是线程在进程内部运行,本质上是在进程的地址空间中运行。
在Linux系统中,CPU听到的PCB(进程控制块)都要比传统的进程愈发轻量化。操作系统可以透过虚拟的进程地址空间,见到进程内部的大部份资源,因而将进程资源合理分给各类执行流,就产生了线程执行流
创建进程:创建进程控制块(task_struct)、进程地址空间(mm_struct)以及页表,虚拟地址和化学地址通过页表来进行映射。每位进程都有自己独立的进程地址空间和页表,也就意味着进程在运行时本身就具有独立性
假如一个进程早已存在,我们再创建一个"进程",我们只为这个进程创建task_struct,并要求创建下来的task_struct和父task_struct共用进程地址空间和页表,这就是所谓的线程。每一个线程就是当前进程中的一个执行流即线程时进程内部的一个执行分支
同时我们可以看出,线程在进程内部运行,本质是线程在进程地址空间中运行,也就是说以前这个进程申请的所有资源,几乎都被所有线程共享
怎样理解进程、线程
通过上述描述我们晓得线程就是一个task_struct,然而进程不仅包含一个或则多个task_struct之外,一个进程还须要右进程地址空间、文件控制块、信号位图、页表等等等,这种合并上去称作一个线程
站在内核的角度:进程是承当分配资源的基本实体,创建进程须要创建进程控制块、创建地址空间、维护页表、并在数学显存中开辟空间构建映射,打开进程默认打开的相关文件、注册讯号等处理方案
站在CPU的角度:线程是CPU调度的基本实体,难以辨识当前调度的task_struct是否是进程,一个CPU只关心一个个独立的执行流,所以无论进程内部有一个或则多个执行流,一个CPU都是根据一个task_struct为单位进行调度的
Linux下不存在真正的多线程,Linux系统内的线程是使用进程模拟的
一个进程内起码存在一个线程,所以线程的数目是少于进程的,线程的执行力度和资源界定比进程要细很多。若果操作系统想要支持线程,就须要构建创建、终止、调度、切换、分配资源、回收资源、释放资源等等线程插口,这一套插口相比进程来说都须要另起炉具,搭建一套与进程平行的更为复杂的线程管理模块。因而假如真要支持线程一定会提升设计操作系统的复杂程度。
所以Linux设计者并没有重新为线程设计数据结构,而是直接复用了进程控制块,所以我们说Linux中所有执行流都称作轻量级进程。既然Linux没有真正意义上的线程,这么也就绝对没有真正意义上的线程相关的系统调用,Linux提供了创建轻量级进程的插口,也就是创建进程,共享空间,其中最具代表性的就是vfork()函数
vfork():创建子进程,而且与兄妹进程共享空间
pid_t vfork(void); // 使用方法类似于fork
#include
#include
#include
using namespace std;
int main(){
int a = 10;
pid_t child_thread = vfork();
if (child_thread == 0) {
a = 20;
cout << "child say : a = " << a << endl;
exit(0);
}
sleep(3);
cout << "father say : a = " << a << endl;
return 0;
}
可以看见,父进程读取到的a是子进程更改后的值,证明了vfork()创建的子进程于父进程是共享地址空间的
原生Pthread库简介
在Linux中,站在内核角度并没有真正意义上的线程相关插口,并且站在用户角度,当用户想要创建一个线程时更希望使用thread_create()类似插口(指向性明晰),而不是类似于vfork()这样的函数(须要了解底层原理),为此系统给用户层提供了原生线程库pthread
原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关插口,对于我们用户来说,学习线程实际就是在使用这一套封装后的插口,而并非操作系统提供的系统调用
理解多级页表
4G显存的机器中,假如页表就是单纯的一张表储存虚拟和化学显存之间的映射关系,这么这张表就须要构建2^32个虚拟地址和化学地址之间的关系,就有2^32个映射项,这么就须要使用2^32*2个表针也就是2^32*2*4个字节(32G)来储存这张表。而且每张表项中不仅储存虚拟地址对应的数学地址外,实际还要储存一些权限相关的信息(用户级页表和内核级页表,就是通过页表中的权限标志位进行分辨的)。这么页表的大小还得继续降低,4G显存的机器根本难以储存32G甚至更大的页表。
所以在32位平台下,页表的映射过程并非直接映射:
1、选择虚拟地址的前十个比特位在页目录中进行查找,找到对应的页表
2、再选定虚拟地址的次十个比特位在对应页表中进行查找,找到化学显存中对应页框的起始位置
3、最后将虚拟地址的剩下12个比特位作为偏斜量从对应页框的起始地址向后进行偏斜,找到化学显存中某一个对应的字节数据
化学显存实际是被界定成,2^12字节也就是4KB大小的页框,c盘上的程序也是被界定成4KB大小的页帧的,当c盘进行数据交互也就是根据4KB大小进行加载和保存的。所以最终我们使用了1张一级页表和2^10张二级页表,设每一条表项10字节,这么只须要使用(2^10+1)*2^10*10差不多10MB就可以将所有页表加载到显存中了
前面所说的所有映射过程,都是由MMU(MemoryManagementUnit显存管理单元)这个硬件来完成的,该硬件被集成在了CPU中。页表是一种软件映射,而MMU是一种硬件映射。所以计算机进行虚拟地址到化学地址的转化采用的是软硬结合的方法
解释常量字符串为何会发生段错误
当我们想要更改一个常量字符串时,虚拟地址必须通过页表映射找到对应的数学显存,而在查表的过程中发觉用户给的地址处于常量区,这个区域的页表权限是只读的,此时若果想要对其进行更改都会在MMU内部触发硬件错误,操作系统在辨识是哪一个进程造成的以后还会向该进程发送讯号另起中止
线程的异同点
优点
缺点
合理使用多线程技术可以提升CPU密集型程序的执行小v了
合理使用多线程技术可以提升IO密集型程序用户的体验(一边看影片一边下载影片,就是多线程运行的一种彰显)
进程VS线程
进程是承当分配资源的实体,线程是CPU调度的基本单位
线程之间似乎共享大部份数据(TextSegment代码端,DataSegment数据段),假如我们定义一个函数,各个线程都可以使用,假如定义一个全局变量,这么所有线程都可以访问。除此之外,各个线程还共享以下资源和环境linux查看硬件信息,文件描述符表(一个线程打开了文件,其它线程也可以见到),每种讯号的处理方法,当前工作目录,当前工作目录,用户ID和组ID
然而任有少部份数据独享:线程ID,一组寄存器(储存上下文信息),栈(储存临时数据),errno(C语言提供的全局变量,每位线程都有自己的),pending位图,讯号屏蔽字,调度优先级是每位线程私有的
Linux线程控制Pthread线程库
pthread线程库是应用层的原生线程库,应用层指的是这个线程库并非系统调用插口提供的linux进程与线程通讯,而是第三方为我们提供的,原生指的是大部份Linux系统就会默认帮我们安装好该线程库
# 库的头文件在 /ust/include/pthread.h
[clx@VM-20-6-centos include]$ pwd
/usr/include
[clx@VM-20-6-centos include]$ ls | grep pthread.h
pthread.h
# 库在 /usr/lib64/libpthread.so
[clx@VM-20-6-centos lib64]$ ls | grep pthread
libevent_pthreads-2.0.so.5
libevent_pthreads-2.0.so.5.1.9
libgpgme-pthread.so.11
libgpgme-pthread.so.11.8.1
libpthread-2.17.so
libpthread.a
libpthread_nonshared.a
libpthread.so
libpthread.so.0
[clx@VM-20-6-centos lib64]$ pwd
/usr/lib64
与线程有关的函数构成了一个完整的系列,绝大多数函数的名子都是以pthread_打头的,在编译阶段须要链接线程函数库
Pthread线程库的错误检测线程创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
主线程创建一个新线程
当一个程序启动时,一个进程被操作系统进行创建,与此同时一个线程也立即运行,这个第一个被创建的线程就是主线程。
即主线程就是形成其它子线程的线程,一般主线程必须最后完成个别执行操作,例如各类关掉动作
小实验
void* Routine(void* arg) {
char* msg = (char*)arg;
while (1) {
printf("I'm child %s, my pid = %d, my father pid = %dn", msg, getpid(), getppid());
sleep(2);
}
return nullptr;
}
void pthread_create_test2(){
pthread_t tids[5];
for (int i = 0; i < 5; i++) {
char* buffer = (char*)malloc(64);
sprintf(buffer, "thread %d", i); // 输出文字到buffer中
pthread_create(tids + i, NULL, Routine, (char*)buffer);
}
while (1) {
printf("I'm main thread, my pid = %d, myppid = %dn", getpid(), getppid());
sleep(2);
}
}
I'm main thread, my pid = 22681, myppid = 13628
I'm child thread 0, my pid = 22681, my father pid = 13628
I'm child thread 1, my pid = 22681, my father pid = 13628
I'm child thread 2, my pid = 22681, my father pid = 13628
I'm child thread 4, my pid = 22681, my father pid = 13628
I'm child thread 3, my pid = 22681, my father pid = 13628
[clx@VM-20-6-centos ~]$ ps -axj | head -1 && ps -axj | grep clxtest | grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13628 22681 22681 13628 pts/11 22681 Sl+ 1001 0:00 ./clxtest
[clx@VM-20-6-centos ~]$ ps -aL | head -1 && ps -aL | grep clxtest | grep -v grep
PID LWP TTY TIME CMD
22681 22681 pts/11 00:00:00 clxtest //可以观察到不同线程LWP是不同的(Light Weight Process)轻量级进程ID
22681 22682 pts/11 00:00:00 clxtest
22681 22683 pts/11 00:00:00 clxtest
22681 22684 pts/11 00:00:00 clxtest
22681 22685 pts/11 00:00:00 clxtest
22681 22686 pts/11 00:00:00 clxtest
通过实验可以看见,我们的线程都是属于同一个进程的,ps-axj命令可以查看当前进程信息,ps-aL命令可以显示当前轻量级进程。默认情况下不带L看见的就是全部进程,而带L就是查看进程内多个轻量级进程
注意:Linux中,应用层线程和内核LWP是一一对应的,实际操作系统调度的时侯采用的是LWP而并非PID,单线程进程中LWP和PID起始是相等的linux进程与线程通讯,所以对于单线程进程来说,调度的PID和LWP是相同的
获取线程LWP
方式1:创建线程时使用输出型参数获得方式2:在线程内部调用pthread_self()函数
I'm main thread, I created child thread 0, child thread tid = 140234376075008 # 使用tid打印
I'm main thread, I created child thread 1, child thread tid = 140234367682304
I'm main thread, I created child thread 2, child thread tid = 140234359289600
I'm main thread, I created child thread 3, child thread tid = 140234350896896
I'm main thread, I created child thread 4, child thread tid = 140234342504192
I'm main thread, my pid = 32015, myppid = 13628
I'm child thread 1, my pid = 32015, my father pid = 13628
I'm child thread 1, my tid = 140234367682304 # 使用pthread_self()打印
I'm child thread 2, my pid = 32015, my father pid = 13628
I'm child thread 2, my tid = 140234359289600
I'm child thread 3, my pid = 32015, my father pid = 13628
I'm child thread 3, my tid = 140234350896896
I'm child thread 4, my pid = 32015, my father pid = 13628
I'm child thread 4, my tid = 140234342504192
I'm child thread 0, my pid = 32015, my father pid = 13628
I'm child thread 0, my tid = 140234376075008
[clx@VM-20-6-centos ~]$ ps -aL | head -1 && ps -aL | grep clxtest | grep -v grep
PID LWP TTY TIME CMD
32015 32015 pts/11 00:00:00 clxtest
32015 32016 pts/11 00:00:00 clxtest # 使用ps -aL 指令获取
32015 32017 pts/11 00:00:00 clxtest
32015 32018 pts/11 00:00:00 clxtest
32015 32019 pts/11 00:00:00 clxtest
32015 32020 pts/11 00:00:00 clxtest
可以观察到我们通过函数获得的tid和内核的LWP的值时不相等的,pthread函数获得的是用户级线程库的线程ID,LWP是内核轻量级进程ID,它们之间是一一对应的关系
线程等待
一个线程被创建就好似进程通常,是须要执行某种特定任务的,用户须要晓得任务处理的如何样了(成功or失败),所以主线程也是须要等待子线程的。假如主线程不对子线程进行等待,这么这个线程的资源也就难以被回收,也会形成类似“僵尸进程”的问题
int pthread_join(pthread_t thread, void **retval); // 注意:pthread_join 函数默认时以阻塞的方式进行等待的
调用该函数可以将线程挂起等待,直至tid=thread的线程中止,但是该线程中止方式不同,pthread_join获得到的中止装填也是不同的
1、如果thread线程通过return返回或则pthread_exit()返回,这么retval参数指向的就是该线程的返回值
2、如果thread线程被其它线程调用pthread_cancel()异常中止掉,retval参数指向的单元储存的就是常数PTHREAD_CANCELED
3、如果对线程的中止状态不感兴趣,也可以传NULL给retval参数
[clx@VM-20-6-centos pthread_api]$ grep -ER "PTHREAD_CANCELED" /usr/include/
/usr/include/pthread.h:#define PTHREAD_CANCELED ((void *) -1) # 可以看到PTHREAD_CANCELED 就是 -1
void* Routine3(void* arg) {
char* msg = (char*)arg;
printf("I'm child %s, my pid = %d, my father pid = %dn", msg, getpid(), getppid());
printf("I'm child %s, my tid = %ldn", msg, pthread_self());
sleep(2);
return (void*)2023;
}
void pthread_join_test1(){
pthread_t tids[5];
for (int i = 0; i < 5; i++) {
char* buffer = (char*)malloc(64);
sprintf(buffer, "thread %d", i); // 输出文字到buffer中
pthread_create(tids + i, NULL, Routine3, (char*)buffer);
printf("I'm main thread, I created child thread %d, child thread tid = %ldn", i, tids[i]);
}
for (int i = 0; i < 5; i++) {
void* ret = NULL;
pthread_join(tids[i], &ret);
printf("thread %d[%lu]...quit, exitcode: %ldn", i, tids[i], (long)ret);
}
}
这么为何线程退出时只能领到线程的退出码,而没有退出讯号以及coredump标志
由于线程同进程一样,线程退出的情况也是代码运行结束,结果正确/不正确,和异常中止。所以我们也必须考虑线程异常中止的情况。并且由于线程是进程的一个执行分支,假如进程中的某个线程崩溃了,整个进程也会因而崩溃。此时还没有执行pthread_join函数,进程就退出了。所以pthread_join函数只能获取到线程正常退出情况下的退出码,用于判定线程运行结果是否正确
线程中止
线程中止有三种方式:从线程函数中return,调用pthread_exit()中止自己,调用pthread_cancel()函数中止同一个进程中的另外一个线程
int pthread_cancel(pthread_t thread);
线程是可以自己取消自己的,取消成功的线程退出码通常会被设置成-1,并且我们通常不这样做,一般都是使用主线程取消新线程。
void* Routine4(void* arg) {
char* msg = (char*)arg;
printf("I'm child %s, my pid = %d, my father pid = %dn", msg, getpid(), getppid());
printf("I'm child %s, my tid = %ldn", msg, pthread_self());
sleep(2);
// pthread_cancel(pthread_self()); // 自己取消自己
return (void*)2023;
}
void pthread_cancel_test1(){
pthread_t tids[5];
for (int i = 0; i < 5; i++) {
char* buffer = (char*)malloc(64);
sprintf(buffer, "thread %d", i); // 输出文字到buffer中
pthread_create(tids + i, NULL, Routine4, (char*)buffer);
printf("I'm main thread, I created child thread %d, child thread tid = %ldn", i, tids[i]);
}
for (int i = 0; i < 5; i++) { // 主线程取消子线程
pthread_cancel(tids[i]);
}
for (int i = 0; i < 5; i++) {
void* ret = NULL;
pthread_join(tids[i], &ret);
printf("thread %d[%lu]...quit, exitcode: %ldn", i, tids[i], (long)ret);
}
}
其实也存在新线程取消主线程的情况
void* Routine5(void* arg) {
pthread_t main_tid = *(pthread_t*)arg;
delete (pthread_t*)arg;
pthread_cancel(main_tid);
int count = 0;
while (count < 5){
count++;
cout << "child thread running..." << endl;
sleep(2);
}
return (void*)2023;
}
// 子线程取消主线程
void pthread_cancel_test2(){
pthread_t tid;
pthread_t* main_id = new pthread_t(pthread_self());
pthread_create(&tid, NULL, Routine5, (void*)main_id);
printf("I'm main thread, I created child thread, child thread tid = %ldn", tid);
void* ret;
pthread_join(tid, &ret);
cout << "child thread quit... exitcode = " << (long)ret << endl;
}
######################################
PID LWP TTY TIME CMD
5920 5920 pts/12 00:00:00 clxtest <defunct> // 可以看到主线程失效
5920 5922 pts/12 00:00:00 clxtest
I'm main thread, I created child thread, child thread tid = 139622965786368 // 主线程失效后子线程任然可以运行
child thread running...
child thread running...
child thread running...
child thread running...
child thread running...
注意:当采用这些方法取消主线程可以发觉,主线程和新线程的地位是对等的。虽然主线程被取消,也不影响其它线程执行后续代码。但正常情况下我们呢都是使用主线程去控制新线程,这样符合我们线程控制的基本逻辑。所以这些技巧并不推荐
线程分离
int pthread_detach(pthread_t thread);
假如在线程执行函数中调用pthread_detach(pthread_self())函数就可以将子线程分离出去,其实这个操作也可以在主线程中执行。被设置的的线程,系统会手动回收对应的线程资源,不须要主线程进行join
线程ID以及进程地址空间分布
线程库虽然就是一个动态库,进程运行动态库被加载到显存,之后通过页表映射到进程地址空间中的共享区,此时进程内部所有线程都可以见到动态库中的数据。我们所说的每位线程都有自己私有的栈,其中不仅主线程采用的栈是进程地址空间原生的栈,其余的线程采用的栈就是在共享区中开辟的。除此之外,每位线程都有自己的structpthread,当中包含了对应线程的各类属性,每位线程还有自己的线程局部储存,当中包含了对应线程被切换时的上下文数据,因而我们想要找到一个用户级线程只须要找到该进程显存块的地址,然后就可以从该结构体中获取线程的各类信息
所以我们所用的各类线程函数,本质就是在库内部对线程执行各类操作,最后将须要执行的代码交给对应的内核级LWP去执行就行了,也就是说线程数据的管理本质是在共享区的
// 使用打印地址的方式打印tid
void* Routine6(void* arg) {
while (1) {
printf("child thread tid : %pn", pthread_self());
sleep(1);
}
}
void pthread_address_test(){
pthread_t tid;
pthread_create(&tid, NULL, Routine6, NULL);
while (1) {
printf("main thread tid : %pn", pthread_self());
sleep(2);
}
}
就可以从该结构体中获取线程的各类信息
所以我们所用的各类线程函数linux修改文件名,本质就是在库内部对线程执行各类操作,最后将须要执行的代码交给对应的内核级LWP去执行就行了,也就是说线程数据的管理本质是在共享区的
// 使用打印地址的方式打印tid
void* Routine6(void* arg) {
while (1) {
printf("child thread tid : %pn", pthread_self());
sleep(1);
}
}
void pthread_address_test(){
pthread_t tid;
pthread_create(&tid, NULL, Routine6, NULL);
while (1) {
printf("main thread tid : %pn", pthread_self());
sleep(2);
}
}