推荐使用线程的理由有哪些
- 线程共享进程的所有资源,如mm_struct,所以线程的创建比进程更快;
- 由于同一个进程的线程间共享mm_struct,所以线程间的数据传输效率会更高,不需要依赖什么通信机制;
- 一个进程中,可以实现程序真正的并行;
- 多CPU系统中,多线程可以真正的并发执行,提高资源利用率
进程和线程的关系:
进程 = = 资源管理 + 线程
线程可以访问的数据
- 代码,.text
- 线程栈,从进程的堆上开辟,大小默认为 8M
- 全局数据
- 线程私有数据,thread_local,对应linux上的接口分别是,
pthread_key_create(),ptread_setspecific(),pthread_getspecific()以及pthread_key_delete()
。
#ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15276
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 15276
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
进程维护线程栈空间
使用两个链表来管理这些线程栈:
- stack_used 维护还没有退出的线程的线程栈
- stack_cache 维护已经退出的线程的线程栈。一般退出的线程的线程栈并不马上销毁,而是缓存在堆中,等下次有新的线程启动了,直接拿来使用即可,不需要重新的申请分配。
Linux系统创建线程的过程
-
用户程序调用Linux库函数,
pthread_create()
; -
试图根据设置用户态的栈的大小从stack_cache找出合适大小的线程栈,如果有,设置为线程栈,如果没有则从堆中申请指定大小的空间作为线程栈;
-
创建一个pthread实例,将这个实例放在线程栈的栈低位置,pthread中存放可当前线程的数据,如线程的私有数据,栈的大小,需要执行的函数等。
-
主线程调用create_thread(),create_thread()将调用clone()产生系统调用,陷入内核
-
将主线程的寄存器信息保存在主线程内核栈中,然后调用sys_clone()
-
紧接着调用do_fork():
创建task_struct,以及对应的内核栈
不需要复制进程的task_struct,共享进程的task_struct,类似于浅拷贝
-
维护亲缘关系:
tgid 所属进程的pid
pid 自己的进程id
如果tgid==pid,那么表示当前task是一个进程,如果tgid!=pid,表示当前task是一个线程。
- 将该线程的task_struct加入task_struct的链表队列中,返回用户空间
这里调用clone()前,进入内核态前,保存的是主线程的栈和指令指针,按照一般的系统调用来看,
返回用户态后,恢复用户态的执行后,应该是恢复主线程的状态。但是这里clone()与一般的系统调用
不一样,clone()返回后,恢复的栈是刚刚创建的线程的栈,栈顶指针等都是指向刚刚创建的线程的栈。
- 调用start_thread(),指向线程函数;
- 直到线程结束,将线程栈归还给stack_cache.
小结
线程的创建是用户空间和内核空间共同配合完成的,上述过程中前三部都是发生在用户态,旨在产生一个用户态的thread实例,后面的步骤发生在内核态,旨在产生一个内核态的thread实例,将用户态和内核态的thread实例一一对应,实际上就是一对一的线程模型。