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大小的页框,磁盘上的程序也是被划分成4KB大小的页帧的,当磁盘进行数据交互也就是按照4KB大小进行加载和保存的。所以最终我们使用了1张一级页表和2 ^ 10 张二级页表,设每一条表项10字节,那么只需要使用 (2 ^ 10 + 1) * 2 ^ 10 * 10 差不多10MB就可以将所有页表加载到内存中了
上面所说的所有映射过程,都是由MMU(MemoryManagementUnit
内存管理单元)这个硬件来完成的,该硬件被集成在了CPU中。页表是一种软件映射,而MMU是一种硬件映射。所以计算机进行虚拟地址到物理地址的转化采用的是软硬结合的方式
解释常量字符串为什么会发生段错误
当我们想要修改一个常量字符串时,虚拟地址必须通过页表映射找到对应的物理内存,而在查表的过程中发现用户给的地址处于常量区,这个区域的页表权限是只读的,此时如果想要对其进行修改就会在MMU内部触发硬件错误,操作系统在识别是哪一个进程导致的之后就会向该进程发送信号另起终止
优点
缺点
合理使用多线程技术可以提高CPU密集型程序的执行小v了
合理使用多线程技术可以提高IO密集型程序用户的体验(一边看电影一边下载电影,就是多线程运行的一种体现)
进程是承担分配资源的实体,线程是CPU调度的基本单位
线程之间虽然共享大部分数据(Text Segment 代码端,Data Segment 数据段),如果我们定义一个函数,各个线程都可以使用,如果定义一个全局变量,那么所有线程都可以访问。除此之外,各个线程还共享以下资源和环境,文件描述符表(一个线程打开了文件,其它线程也可以看到),每种信号的处理方式,当前工作目录,当前工作目录,用户ID和组ID
但是任有少部分数据独享:线程ID,一组寄存器(存储上下文信息),栈(存储临时数据),errno
(C语言提供的全局变量,每个线程都有自己的), pending位图,信号屏蔽字,调度优先级是每个线程私有的
Pthread
线程库pthread
线程库是应用层的原生线程库,应用层指的是这个线程库并非系统调用接口提供的,而是第三方为我们提供的,原生指的是大部分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
线程库的错误检查erron
赋值以指示错误pthread
系列函数出错时并不会设置全局变量(大部分POSIX函数会这样做),pthread
系列函数会将错误代码通过返回值返回pthread
同样提供了线程内的errno
变量,以支持其它使用errno
的代码,对于pthread
函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno
变量开销要更小int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
thread : 输出型参数,用于获取创建成功线程的ID,该参数是一个输出型参数
attr
(attribute 属性) : 用于设置创建线程的属性,传入NULL设置默认属性
start_routine(routine 常规) : 该参数是一个函数指针,即线程启动后需要执行的函数
arg
(argument 参数) : 传给线程的参数
线程创建成功返回0,失败返回错误码
主线程创建一个新线程
当一个程序启动时,一个进程被操作系统进行创建,与此同时一个线程也立刻运行,这个第一个被创建的线程就是主线程。
即主线程就是产生其它子线程的线程,通常主线程必须最后完成某些执行操作,比如各种关闭动作
小实验
void* Routine(void* arg) {
char* msg = (char*)arg;
while (1) {
printf("I'm child %s, my pid = %d, my father pid = %d\n", 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 = %d\n", 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起始是相等的,所以对于单线程进程来说,调度的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 函数默认时以阻塞的方式进行等待的
thread 需要等待的线程ID
retval
: 线程退出时的退出码信息
成功返回0,失败返回错误码
调用该函数可以将线程挂起等待,直到 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 = %d\n", msg, getpid(), getppid());
printf("I'm child %s, my tid = %ld\n", 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 = %ld\n", i, tids[i]);
}
for (int i = 0; i < 5; i++) {
void* ret = NULL;
pthread_join(tids[i], &ret);
printf("thread %d[%lu]...quit, exitcode: %ld\n", i, tids[i], (long)ret);
}
}
那么为什么线程退出时只能拿到线程的退出码,而没有退出信号以及core dump标志
因为线程同进程一样,线程退出的情况也是 代码运行结束,结果正确/不正确,和异常终止。所以我们也必须考虑线程异常终止的情况。但是因为线程是进程的一个执行分支,如果进程中的某个线程崩溃了,整个进程也会因此崩溃。此时还没有执行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 = %d\n", msg, getpid(), getppid());
printf("I'm child %s, my tid = %ld\n", 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 = %ld\n", 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: %ld\n", 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 = %ld\n", 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
pthread_create()
会产生一个线程ID,该线程ID和内核的LWP不一样。内核中的LWP属于进程调度范畴,因为线程是轻量级进程,是操作系统最小单位,需要一个数值来唯一标识该进程pthread_create()
函数第一个参数指向一个虚拟内存单元,该单元的地址即为新创建线程ID,这地址在NPTL线程库中,线程库后续就是通过这个地址来读取线程数据来操作线程的线程库其实就是一个动态库,进程运行动态库被加载到内存,然后通过页表映射到进程地址空间中的共享区,此时进程内部所有线程都可以看到动态库中的数据。我们所说的每个线程都有自己私有的栈,其中除了主线程采用的栈是进程地址空间原生的栈,其余的线程采用的栈就是在共享区中开辟的。除此之外,每个线程都有自己的struct pthread
,当中包含了对应线程的各种属性,每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据,因此我们想要找到一个用户级线程只需要找到该进程内存块的地址,之后就可以从该结构体中获取线程的各种信息
所以我们所用的各种线程函数,本质就是在库内部对线程执行各种操作,最后将需要执行的代码交给对应的内核级LWP去执行就行了,也就是说线程数据的管理本质是在共享区的
// 使用打印地址的方式打印tid
void* Routine6(void* arg) {
while (1) {
printf("child thread tid : %p\n", pthread_self());
sleep(1);
}
}
void pthread_address_test(){
pthread_t tid;
pthread_create(&tid, NULL, Routine6, NULL);
while (1) {
printf("main thread tid : %p\n", pthread_self());
sleep(2);
}
}
就可以从该结构体中获取线程的各种信息
所以我们所用的各种线程函数,本质就是在库内部对线程执行各种操作,最后将需要执行的代码交给对应的内核级LWP去执行就行了,也就是说线程数据的管理本质是在共享区的
// 使用打印地址的方式打印tid
void* Routine6(void* arg) {
while (1) {
printf("child thread tid : %p\n", pthread_self());
sleep(1);
}
}
void pthread_address_test(){
pthread_t tid;
pthread_create(&tid, NULL, Routine6, NULL);
while (1) {
printf("main thread tid : %p\n", pthread_self());
sleep(2);
}
}