我在前面的文章中提到了linux产生进程的三个系统调用:
1.fork() 用来产生拥有独立的虚拟地址空间的子进程,遵循 读时共享写时拷贝 原则
2.vfork() 产生的子进程与父进程共享数据,读写共享,通常搭配exec()或者exit(),否则会死锁
3.clone() linux下用来产生线程的系统调用,我们使用的pthread_create()调用的就是它
线程的基础内容在这篇文章中有提到:
https://blog.csdn.net/KingOfMyHeart/article/details/90261251
一、线程的基础补充内容:加深理解
1.linux下的线程本质还是进程,拥有自己的PCB,只不过地址空间共享;
(可以说是线程是"合租",进程是"独居",主线程也就是一开始的进程的地址空间即为"合租房")
linux上加入线程感觉是比较牵强的,并不是我们想象中的那样.emmmm
2.进程是分配资源的最小单位,线程是最小的调度单位
在没有引入线程的概念时,进程的作用是统资源分配,CPU最小调度单位
而引入线程后,线程继承了“调度单位”,从此以后,进程依旧是资源分配单位,线程成了最小调度单位
二、线程的内核实现原理:线程 LWP
1.从内核的角度来看,进程和线程是一样的,线程都有自己的PCB(在内核的眼里就伪装成了进程),但是PCB指向的三级页表是相同的;
2.线程的执行 表现在函数调用上,所以我们可以认为线程是寄存器和栈的集合;
从图上看,尽管一个进程中的线程三级页表一样,但是每个线程执行的函数不一样,
这样就足够了,绝对不会发生内存冲突问题。
所以,可以认为线程与线程之间的区别最大的是 每个线程对应的函数栈空间不一样
实际上,内存映射表保存着内核空间,最终是由MMU将虚拟内存映射到真是的物理内存上
3.进程蜕变成线程:
当一个进程主动的调用pthread_create()时,这个进程我们一般称为主线程
有“独居” 变为 “合租”
4.操作系统区分线程使用的是LWP 线程号:
查看一个进程开启的所有线程:
ps -aux 找到该进程进程号
ps -Lf 进程号 即可得到该进程开启的所有的线程号
注意:这里是线程号,而不是线程id
线程号:cpu用来分配时间片用的
线程id:进程内部区分线程
5.线程之间共享的资源和不共享的资源是哪些:
共享的资源包括:
1.文件描述符表
2.每种信号的处理方式(捕捉、忽略、默认都是共享的)
3.当前的工作目录 和 用户ID 组ID
4.内存地址空间(text/data/heap/bss/共享库)
不共享资源包括:
1.线程ID
2.处理器现场和栈指针(内核栈)
3.独立的栈空间(用户栈)
4.errno变量: 虽然是全局变量,但是依然是私有的
5.信号屏蔽字
6.调度优先级
6.线程的优缺点:
优点:并发性提高、开销小(内核工作量小)、数据通信和共享数据方便
缺点:库函数不稳定(库函数不如系统调用稳定高)、调试困难、对信号支持不好
7.内核创建线程函数原型:
asmlinkage int sys_fork(struct pt_regs *regs)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return(-EINVAL);
#endif
}
asmlinkage int sys_vfork(struct pt_regs *regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);
}
//以上函数是创建进程用的
//对照产生进程的函数来看,实际上调用的都是do_fork(),根据传递的参数不同,产生的进程不同
/* Clone a task - this clones the calling program thread.
* This is called indirectly via a small wrapper
*/
asmlinkage int sys_clone(unsigned long clone_flags, unsigned long newsp,
int __user *parent_tidptr, int tls_val,
int __user *child_tidptr, struct pt_regs *regs)
{
if (!newsp)
newsp = regs->ARM_sp;
return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
}
三、线程的控制原语:
头文件#include<pthread.h>
编译链接使用 -lpthread
1.pthread_self
函数原型: pthread_t pthread_self(void);
作用:获取线程的ID
对应关系:在进程中的函数是 getpid()
返回值:成功返回0,失败不返回
线程ID是进程内部的标识标志,(两个进程之间,线程的ID可以相同)
2.pthread_create
函数原型: int pthread_create(pthread_t *thread, //传出参数,产生新线程的ID
const pthread_attr_*attr //线程属性,一般为NULL
void*(start_routine)(void*)
//线程的工作函数,是一个函数指针,当该函数运行结束,表示这个线程结束
void*arg //线程的工作函数的参数 );
作用:创建一个新线程
对应关系:在进程中的函数是 fork()
返回值:成功返回0,失败返回错误编号
3.pthread_exit
函数原型:void pthread_exit(void *retval); retval表示线程退出状态,一般为NULL
作用:创建单个线程退出
对应关系:在进程中的函数是 exit() 退出整个进程
返回值:成功返回0,失败返回错误编号
我们可以将主线程中调用pthread_exit()单独的退出,派生线程不受影响
主函数main中:return 和 exit都是 将整个进程退出,return是返回,主函数中返回自然是退出进程了
如果我们在线程的工作函数中加上exit:依然是将整个进程退出
exit()是将进程退出 pthread_exit()是将线程退出 return 是将函数返回
所以我们在多线程中谨慎使用exit()和return,如果要退出线程,使用pthread_exit()即可
4.pthread_join
函数原型:int pthread_join(pthread_t thread,void **retval);
thread表示线程ID,retval存储的是线程结束状态,一般是线程函数return时的返回值
作用:主线程阻塞的等待子线程的退出,获取退出码
对应关系:在进程中的函数是 waitpid()
返回值:成功返回0,失败返回错误编号
5.pthread_detach
线程的分离状态:子线程主动与主线程断开关系,线程结束后,其状态不由其他线程获取,而直接自己释放。网络、多线程服务器常用。
函数原型:int pthread_detach(pthread_t thread);
thread表示线程ID
作用:从状态上分离出来,并不会独占地址空间,仅仅是状态上。
好处是结束后不会产生僵尸线程,结束自己将自己的PCB从内存中清理掉,父线程不用等待改类型子线程。
对应关系:没有
返回值:成功返回0,失败返回错误编号
6.pthread_cancel
函数原型:int pthread_cancel(pthread_t thread);
thread表示线程ID
作用:将线程杀死
对应关系:对应进程的操作原语是 kill()
返回值:成功返回0,失败返回错误编号