在创建一个进程时,操作系统做了以下的工作:
1、创建一个进程所需要的各种数据结构(pcb、地址空间、页表、与文件的关系,父子关系等等)
2、开辟物理内存,把硬盘上的代码和数据加载到物理内存中。
3、把虚拟地址空间和物理内存通过页表建立映射关系。
所以,创建一个进程就是一个执行流从无到有的过程。在这个过程中,我们申请了很多资源,创建了很多东西。
当CPU调度时,要找到对应进程的代码和数据。
如果再想创建一个进程,重复上述过程。
进程的概念:是承担分配系统资源的基本实体。(从操作系统观点),为了承担系统资源,操作系统为进程创建了大批数据结构和大批的内存块,以承载该进程的代码和数据。
线程:是调度的基本单位,线程是进程内部的执行流!
什么叫做线程在进程内部?
进程和线程的对应关系应该是一对多,一个进程内有多个执行流(线程)。
站在线程的角度,怎么理解之前的单进程?
现在应该如何看待task_struct呢?
从CPU角度,现在它看到的task_struct都要比传统的进程更加轻量化,因此,线程也叫作轻量级进程。
1、创建一个新线程的代价要比创建一个新进程小得多
2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3、线程占用的资源要比进程少很多
4、能充分利用多处理器的可并行数量
5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1、性能损失
2、健壮性降低
3、缺乏访问控制
4、编程难度提高
线程的管理依靠的是pthread库。
创建一个线程,应用的是函数pthread_create
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);
参数:
thread:返回线程ID(操作系统不知道这个id,是由库提供的)
attr:设置线程的属性,一般设置为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
且生成的可执行程序是动态链接的。
./mythread
是一个进程,但有两个执行流,如何证明这两个执行流来自一个进程?
在上图,我们发现有一个重要标识LWP (light weight process,轻量级进程):对进程的标识作用,调度时用的是LWP。
我们知道Linux中没有真正意义上的线程,在对线程的管理是依靠pthread库来完成的。要管理就要描述和组织线程,需要有描述线程的结构体,在用户区创建一个线程的实体,但在操作系统中需要有一个执行流跟它关联起来,也就是LWP。
线程实体与LWP是有对应关系的。可以简单理解为用户区线程实体结构体中包含对应的轻量级进程ID。
不同的操作系统对应关系是不一样的,在Linux中是一一对应的。
总结为,操作系统底层为我们提供线程的执行功能;而用户级别提供用户空间的线程管理功能。
这个ID绝不是前面提的轻量级进程ID! LWP是系统层面的,而该ID是用户层面,也就是库提供的。
pthread_create函数的第一个参数,指向的是一个虚拟的内存单元,该内存单元的地址即为新创建线程的线程ID。
也就说,动态库要被加载到共享区中,库里一定会包含描述线程的结构体,把这些结构体按照数组的形式组织在一起。线程ID可以理解成该线程在库当中,描述它自身结构的结构体的内存位置的起始地址。 这也就解释的通,为什么上图中打印出来的ID这么像地址!
创建一个进程,此时该进程只有一个执行流,pthread_create执行完毕后,创建了新的线程,此时有了两个执行流。
新线程执行时如果出现除零错误,操作系统识别到硬件异常,确定是哪个进程让我的CPU执行除零,向对应进程发送信号【注意:信号发送的基本单位是进程!】,然后进程的资源就被释放掉了,此时在进程中滋生的一堆线程也要使用这些资源,但资源已被释放,所以这些线程也就随之崩溃了。
因此,线程健壮性不强,只要众多中有一个出现崩溃,整个都会崩溃。
1、合理的使用多线程,能提高CPU密集型程序的执行效率。
2、合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
进程与线程相比:
进程更强调独立性,但又不是绝对独立,比如fork创建子进程,进程间通信等等,都会出现资源共享的情况。
线程更强调共享,但又不是绝对共享,还有部分是自己私有的:线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级。
其中,要特别注意:线程私有线程运行栈!每个线程都有自己的运行栈。如果栈是共享的话,栈的FILO特性会让整个数据变得混乱;每个线程都有一套自己的寄存器!(也叫硬件上下文/任务状态段tss)这个证明线程是会被切换的。
各线程会共享进程资源和环境:文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)、当前工作目录、用户id和组id。
新线程必须被主线程等待,如果不等待就会造成类似于僵尸进程这样的问题,导致内存泄漏。
默认都是以阻塞方式等待。
关于线程的等待,我们并不用关心异常的情况,因为默认是不会错的,一旦出现错误,就是进程要去处理的。会给进程发信号,所以信号是专门为进程设计的。
int pthread_join(pthread_t thread, void **value_ptr);
thread:线程ID
value_ptr:它指向一个指针,该指针指向被等待线程的返回值。
返回值:成功返回0;失败返回错误码
#include
#include
#include
#include
#include
void* thread_run(void* arg)
{
printf("%s, %lu, pid:%d \n",(char*)arg, pthread_self(), getpid());
sleep(5);
return (void*)10;
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_run, "thread 1");
printf("main : %lu, pid:%d\n",pthread_self(), getpid());
void* ret = NULL;
pthread_join(tid, &ret);
printf("thread quit code: %d\n",(long long)ret);
}
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
void pthread_exit(void *value_ptr);
value_ptr:value_ptr不要指向一个局部变量。因为当其它线程得到这个返回指针时线程函数已经退出了。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
int pthread_cancel(pthread_t thread);
thread:线程ID
返回值:成功返回0;失败返回错误码
int pthread_detach(pthread_t thread);
如果不关心线程的返回值时,可以对线程分离。告诉系统,当线程退出时,自动释放线程资源。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。