🏆个人主页:企鹅不叫的博客
🌈专栏
- C语言初阶和进阶
- C项目
- Leetcode刷题
- 初阶数据结构与算法
- C++初阶和进阶
- 《深入理解计算机操作系统》
- 《高质量C/C++编程》
- Linux
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
💙系列文章💙
【Linux】第一章环境搭建和配置
【Linux】第二章常见指令和权限理解
【Linux】第三章Linux环境基础开发工具使用(yum+rzsz+vim+g++和gcc+gdb+make和Makefile+进度条+git)
【Linux】第四章 进程(冯诺依曼体系+操作系统+进程概念+PID和PPID+fork+运行状态和描述+进程优先级)
【Linux】第五章 环境变量(概念补充+作用+命令+main三个参数+environ+getenv())
【Linux】第六章 进程地址空间(程序在内存中存储+虚拟地址+页表+mm_struct+写实拷贝+解释fork返回值)
【Linux】第七章 进程控制(进程创建+进程终止+进程等待+进程替换+min_shell)
【Linux】第八章 基础IO(open+write+read+文件描述符+重定向+缓冲区+文件系统管理+软硬链接)
【Linux】第九章 动态库和静态库(生成原理+生成和使用+动态链接)
【Linux】第十章 进程间通信(管道+system V共享内存)
【Linux】第十一章 进程信号(概念+产生信号+阻塞信号+捕捉信号)
文章目录
- 💙系列文章💙
- 💎一、线程概念
- 🏆1.线程定义
- 🏆2.线程的优点和缺点
- 🏆3.线程异常
- 🏆4.线程用途
- 🏆5.二级页表
- 💎二、线程和进程
- 💎三、线程控制
- 🏆1.POSIX线程库
- 🏆2.线程创建
- pthread_create
- 🏆3.进程ID和线程ID
- pthread_self
- 🏆4.线程ID及进程地址空间布局
- 🏆5.线程终止
- pthread_exit
- pthread_cancel
- 🏆6.线程等待
- pthread_join
- 🏆7.线程分离
💎一、线程概念
🏆1.线程定义
- 线程是OS能够进行运算调度的基本单位。更准确的定义是:线程是“一个进程内部的控制序列”。
- 一切进程至少都有一个执行线程。
- 线程在进程内部运行,本质是在进程地址空间内运行。
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
- Linux下没有真正意义上的线程,线程是通过进程来模拟实现的
这样可以看出一个线程就是一个执行流,每一个线程有一个task_struct的结构体,和进程一样,这些task_struct都是由OS进行调度,线程在进程内部运行,本质就是线程在进程地址空间内运行,也就是说曾经这个进程申请的所有资源,几乎都是被所有线程共享的。。
红色框框是进程,除了task_struct之外,一个进程还要有进程地址空间、文件、信号等等,合起来称之为一个进程。承担分配系统资源的基本实体
🏆2.线程的优点和缺点
优点:
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用(执行流的大部分任务,主要以计算为主),为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用(执行流的大部分任务,主要以IO为主),为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
缺点:
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。
🏆3.线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃,所以一个线程异常会影响整个进程
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
🏆4.线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率。
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。
🏆5.二级页表
以32位平台为例,在32位平台下一共有232个地址,也就意味着有232个地址需要被映射。
- 选择虚拟地址的前10个比特位在页目录当中进行查找,找到对应的页表。
- 再选择虚拟地址的10个比特位在对应的页表当中进行查找,找到物理内存中对应页框的起始地址。
- 最后将虚拟地址中剩下的12个比特位作为偏移量从对应页框的起始地址处向后进行偏移,找到物理内存中某一个对应的字节数据。
- 物理内存实际是被划分成一个个4KB大小的页框的,4KB实际上就是212个字节,也就是说一个页框中有212个字节,而访问内存的基本大小是1字节,因此一个页框中就有212个地址
- 页目录项是一级页表,页表项是二级页表
- 每一个表项还是按10字节计算,页目录和页表的表项都是210个,因此一个表的大小就是210 * 10个字节,也就是10KB
- 而页目录有210个表项也就意味着页表有210个,也就是说一级页表有1张,二级页表有210张,总共算下来大概就是10MB
修改常量字符串会报错:修改一个字符串常量时,虚拟地址必须经过页表映射找到对应的物理内存,而在查表过程中发现其权限是只读的就会报错
💎二、线程和进程
进程是承担分配系统资源的基本实体,线程是调度的基本单位,进程具有独立性,但线程之间可以互相影响。
线程共享一部分进程数据,也有自己独有的一部分数据:
- 线程ID
- 一组寄存器(记录上下文信息,任务状态段)
- 栈(线程私有栈),没有堆
- errno(错误码)
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间:
- 如果定义一个函数,在各线程中都可以调用。
- 如果定义一个全局变量,在各线程中都可以访问到。
- 文件描述符表。(进程打开一个文件后,其他线程也能够看到)
- 每种信号的处理方式。(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
- 当前工作目录。(cwd)
- 用户ID和组ID。
关系图:
💎三、线程控制
🏆1.POSIX线程库
- 要使用这些函数库,要通过引入头文件<pthreaad.h>。
- 链接这些线程函数库时,要使用编译器命令的“-lpthread”选项。
错误检查:pthreads函数出错时不会设置全局变量errno,而是将错误代码通过返回值返回
🏆2.线程创建
pthread_create
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;失败返回错误码
实例:让主线程调用pthread_create函数创建一个新线程,此后新线程就会跑去执行自己的新例程,而主线程则继续执行后续代码
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* pthreadrun(void* arg) { char* msg = (char*)arg; while (1){ printf("I am %s, PID:%d\n", msg,getpid()); sleep(1); } } int main() { pthread_t tid; pthread_create(&tid, NULL, pthreadrun, (void*)"thread 1"); while (1){ printf("I am main thread!, PID:%d\n",getpid()); sleep(2); } return 0; }
结果:新线程每隔一秒执行一次打印操作,而主线程每隔两秒执行一次打印操作
[root@VM-20-8-centos:/home/Jungle/lesson32]# ./main I am main thread!, PID:30083 I am thread 1, PID:30083 I am thread 1, PID:30083 I am main thread!, PID:30083 I am thread 1, PID:30083 I am thread 1, PID:30083
使用
ps -aL
命令,可以显示当前的轻量级进程,发现这两个线程都属于同一个进程[Jungle@VM-20-8-centos:~]$ ps -aL PID LWP TTY TIME CMD 21028 21028 pts/6 00:00:00 sudo 21029 21029 pts/6 00:00:00 scl 21030 21030 pts/6 00:00:00 bash 21035 21035 pts/6 00:00:00 bash 30083 30083 pts/6 00:00:00 main 30083 30084 pts/6 00:00:00 main 30163 30163 pts/8 00:00:00 ps
🏆3.进程ID和线程ID
引入线程概念之后,一个用户进程下管理多个用户态线程,每个线程作为一个独立的调度实体,在内核中都有自己的进程描述符。进程和线程关系变成了1:N的关系
多线程的进程,又被称为线程组。线程组内的每一个线程在内核中都有一个进程描述符与之对应。进程描述符结构体表面上看是进程的pid,其实它对应的是线程ID;进程描述符中的tpid,含义是线程组ID,该值对应的是用户层面的进程ID
进程是资源的分配单位,所以线程并不拥有系统资源,而是共享使用进程的资源,进程的资源由系统进行分配
struct task_struct { ... pid_t pid;// 对应的是线程ID,就是我们看到的lwp pid_t tgid;// 线程组ID,该值对应的是用户层面的进程ID ... struct task_struct *group_leader; ... struct list_head thread_group; ... };
具体关系:
用户态 系统调用 内核进程描述符中对应的结构 线程ID pid_t gettid(void) pid_t pid 进程ID pid_d getpid(void) pid_t tgid 注意:这里的线程ID是属于内核中的LWP属于进程调度的范畴,用来标识轻量级进程
观察以下代码,LWP是内核的轻量级进程ID,第一个线程的ID和进程ID是一样的,这个线程就是主线程
注意: 线程和进程不一样,进程有父进程的概念,但是在线程组中,所有的线程都是对等关系。
[Jungle@VM-20-8-centos:~/lesson33]$ ps -aL PID LWP TTY TIME CMD 20966 20966 pts/11 00:00:00 main 20966 20967 pts/11 00:00:00 main
pthread_self
pthread_t pthread_self(void);
功能: 获取线程自身ID
返回值: 线程自身ID
实例:下面代码中在每一个新线程被创建后,主线程都将通过输出型参数获取到的线程ID进行打印,此后主线程和新线程又通过调用pthread_self函数获取到自身的线程ID进行打印
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* pthreadrun(void* arg) { char* msg = (char*)arg; while (1){ printf("I am %s, PID:%d TID:0x%x\n", msg,getpid(), pthread_self()); sleep(1); } } int main() { pthread_t tid1; pthread_t tid2; pthread_create(&tid1, NULL, pthreadrun, (void*)"thread 1"); printf("thread 1 tid is 0x%x\n", tid1); pthread_create(&tid2, NULL, pthreadrun, (void*)"thread 2"); printf("thread 2 tid is 0x%x\n", tid2); while (1){ printf("I am main thread!, PID:%d TID:0x%x\n",getpid(), pthread_self()); sleep(2); } return 0; }
结果:这两种方式获取到的线程的ID是一样的
[Jungle@VM-20-8-centos:~/lesson33]$ ./main thread 1 tid is 0x356c1700 thread 2 tid is 0x34ec0700 I am main thread!, PID:17699 TID:0x36754740 I am thread 2, PID:17699 TID:0x34ec0700 I am thread 1, PID:17699 TID:0x356c1700 I am thread 2, PID:17699 TID:0x34ec0700 I am thread 1, PID:17699 TID:0x356c1700
🏆4.线程ID及进程地址空间布局
- 对于刚刚说的pthread_create函数产生的线程ID和LWP中的内核轻量级线程ID是不一样的
- 内核中的LWP属于进程调度的范畴,用来标识轻量级进程
- pthread_create函数第一个参数是一个地址,指向一个虚拟内存单元,这个地址就是线程的ID,这个ID属于NPTL线程库的范畴,线程库的后续操作就是根据该线程ID来操作线程的。
- 线程库NPTL提供的pthread_self函数,获取的线程ID和pthread_create函数第一个参数获取的线程ID是一样的。
- pthread_t的类型是线程ID,本质是进程地址空间的一个地址
- 线程没有独立的虚拟地址空间,一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的栈 (stack),堆 (heap)是共用的
- 进程运行时动态库被加载到内存,然后通过页表映射到进程地址空间中的共享区,此时该进程内的所有线程都是能看到这个动态库的
- 线程ID都代表的是每一个线程控制块的起始地址,每个线程都有自己的struct pthread,所有的线程可以看成是一个大的数组,被描述组织起来
- 每一个线程都有自己的栈,主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈就是在共享区中开辟的
- 上面所用的各种线程函数,本质都是在库内部对线程属性进行的各种操作,线程数据的管理本质是在共享区的
🏆5.线程终止
想要终止某个线程而不是整个进程有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit
void pthread_exit(void *retval);
功能: 线程终止
参数:
- retval:不能指向局部变量
- 该函数无返回值,跟进程一样,线程结束的时候无法返回它的调用者(自身)。
- pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。
实例:三秒后,使用pthread_exit函数终止线程
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* pthreadrun(void* arg) { int count = 0; char* msg = (char*)arg; while (1){ printf("I am %s, PID:%d TID:0x%x\n", msg,getpid(), pthread_self()); sleep(1); if (++count == 3){ pthread_exit(NULL); } } } int main() { pthread_t tid; pthread_create(&tid, NULL, pthreadrun, (void*)"thread 1"); while (1){ printf("I am main thread!, PID:%d TID:0x%x\n",getpid(), pthread_self()); sleep(1); } return 0; }
结果:线程被终止退出了
[Jungle@VM-20-8-centos:~/lesson33]$ ./main I am main thread!, PID:20971 TID:0x69c48740 I am thread 1, PID:20971 TID:0x68bb5700 I am thread 1, PID:20971 TID:0x68bb5700 I am main thread!, PID:20971 TID:0x69c48740 I am thread 1, PID:20971 TID:0x68bb5700 I am main thread!, PID:20971 TID:0x69c48740 I am main thread!, PID:20971 TID:0x69c48740 I am main thread!, PID:20971 TID:0x69c48740
pthread_cancel
int pthread_cancel(pthread_t thread);
功能: 取消一个线程
参数:
- thread:线程ID
返回值: 成功返回0,失败返回错误码
实例:三秒后,使用pthread_cancel函数终止线程
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* pthreadrun(void* arg) { char* msg = (char*)arg; while (1){ printf("I am %s, PID:%d TID:0x%x\n", msg,getpid(), pthread_self()); sleep(1); } } int main() { pthread_t tid; pthread_create(&tid, NULL, pthreadrun, (void*)"thread 1"); int count = 0; while (1){ printf("I am main thread!, PID:%d TID:0x%x\n",getpid(), pthread_self()); sleep(1); if (++count == 3){ pthread_cancel(tid); printf("new thread is canceled...\n"); } } return 0; }
结果:线程被终止退出了
[Jungle@VM-20-8-centos:~/lesson33]$ ./main I am main thread!, PID:22472 TID:0xb4f87740 I am thread 1, PID:22472 TID:0xb3ef4700 I am main thread!, PID:22472 TID:0xb4f87740 I am thread 1, PID:22472 TID:0xb3ef4700 I am main thread!, PID:22472 TID:0xb4f87740 I am thread 1, PID:22472 TID:0xb3ef4700 new thread is canceled... I am main thread!, PID:22472 TID:0xb4f87740 I am main thread!, PID:22472 TID:0xb4f87740
🏆6.线程等待
等待原因:
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
pthread_join
int pthread_join(pthread_t thread, void **retval);
功能: 等待一个线程结束
参数:
- thread:线程ID
- retval:输出型参数,指向线程退出的返回值
返回值: 成功返回0,失败返回错误码
实例:主函数使用pthread_join函数等待另一个线程,三秒后,线程终止并返回6666
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* pthreadrun(void* arg) { int count = 0; char* msg = (char*)arg; while (1){ printf("I am %s, PID:%d TID:0x%x\n", msg,getpid(), pthread_self()); sleep(1); if (++count == 3){ pthread_exit((void*)6666); } } } int main() { pthread_t tid; pthread_create(&tid, NULL, pthreadrun, (void*)"thread 1"); printf("main thread is waiting new thread\n"); void* ret = NULL; pthread_join(tid, &ret); printf("new thread has exited, exit code is %ld\n", (long)ret); return 0; }
结果:线程终止并返回退出码
[Jungle@VM-20-8-centos:~/lesson33]$ ./main main thread is waiting new thread I am thread 1, PID:23260 TID:0x7e815700 I am thread 1, PID:23260 TID:0x7e815700 I am thread 1, PID:23260 TID:0x7e815700 new thread has exited, exit code is 6666
注意: pthread_join函数默认是以阻塞的方式进行线程等待的,只能获取到线程正常退出时的退出码,用于判断线程的运行结果是否正确。
总结:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED(-1)。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
🏆7.线程分离
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
功能: 对一个线程进行分离
参数:
- thread:线程ID
返回值: 成功返回0,失败返回错误码
实例:创建一个个新线程后让这个新线程将自己进行分离,那么此后主线程就不需要在对这这个新线程进行join了
#include <stdio.h> #include <pthread.h> #include <unistd.h> void* pthreadrun(void* arg) { int count = 0; pthread_detach(pthread_self()); char* msg = (char*)arg; while (1){ printf("I am %s, PID:%d TID:0x%x\n", msg,getpid(), pthread_self()); sleep(1); if (++count == 3){ pthread_exit(NULL); } } } int main() { pthread_t tid; pthread_create(&tid, NULL, pthreadrun, (void*)"thread 1"); sleep(1); void* ret = NULL; pthread_join(tid, &ret); if(pthread_join(tid, &ret) == 0){ printf("success new thread has exited, exit code is %ld\n", (long)ret); }else{ printf("failed\n"); } return 0; }
结果:这个新线程在退出时,系统会自动回收对应线程的资源,不需要主线程进行join
[Jungle@VM-20-8-centos:~/lesson33]$ ./main I am thread 1, PID:24725 TID:0xce554700 failed