【Linux】线程和进程?开始多线程编程喽

请添加图片描述

1 Linux线程概念

1.1 什么是线程

  • 在一个进程中的一个执行流叫做线程,即线程是“一个进程内部的控制序列”
  • 所有进程至少都有一个线程(执行流)
  • 线程在进程内部运行,本质是在进程地址空间中运行
  • 在Linux系统中,CPU看到的task_struct要比真正的PCB更加轻量化
  • 进程将资源合理分配给每个执行流,就形成了线程执行流
  • 站在内核的角度,进程是承担分配系统资源的基本实体。

![[Pasted image 20220209214148.png]]

简单介绍页表映射

在32位平台下,有 2 32 2^{32} 232地址,假设一对映射需要10字节空间的话,那么 2 32 2^{32} 232对映射关系需要 2 32 ∗ 10 2^{32}*10 23210字节,也就是40GB,这实在是太大了,所以系统不是这个设计的。

在32位平台下采用的时候页目录+二级页表的方式。

一个地址有32个比特位,首先根据前10个比特位在有 2 10 2^{10} 210对映射关系的页目录中找到映射地址,然后根据这个地址找到对应的二级页表,根据32位地址中的次10个比特位在有 2 10 2^{10} 210对映射关系的二级页表中找到映射地址。最后根据这个地址在物理内存中到一个页框的首地址(物理内存的基本单位是页框,大小为 2 12 2^{12} 212)然后根据32位地址中的最后12个比特位在这个页框中找到物理内存的偏移位置,这样就可以找到对应的位置。

其中页目录和二级页表所有的映射关系都映射起来最多只有 2 20 2^{20} 220,也就是1MB。主要是由于物理内存页框的特殊特技可以省去使用 2 20 2^{20} 220个有 2 12 2^{12} 212对映射关系的页表了。类似的,64位下使用的是多级页表来进行相似的映射。

而且Linux采用软件页表+硬件MMU(memory manage unit,内存管理单元)来完成虚拟地址到物理地址的映射的。

缺页中断

如果访问期间,目标资源不在内存,则会出发缺页中断,进行内存分配,页表创建,建立映射关系。

比如说在malloc的时候,其实系统根本没有给进程分配物理内存,也没有建立页表映射,只不过是在虚拟内存中分配了一块空间而已,只有当用户使用这块空间的时候,才会在物理内存中开辟空间,建立页表中的映射关系。

站在CPU的角度,能否识别task_struct是进程还是线程?

不能,CPU只关心独立的执行流,也就是进程中的线程。

Linux下不存在真正意义上的多线程

当线程足够多的时候,操作系统就需要使用特定的数据结构来管理线程。但是Linux下并没有这样的结构。所以在Linux下的线程是由进程模拟出来的。

在CPU调度的时候,看到的都是struct thread_info这个结构体,结构体中包含了一个变量struct task_struct

所以task_struct要比进程控制块包含的内容要少。

在Linux中的所有执行流,都叫做“轻量级进程”。

如何使用线程?

因为Linux中没有真正意义上的线程,所以Linux中也就没有真正意义上的线程相关的系统调用。但是提供了创建轻量级进程的接口。

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
  • 作用
    • 创建一个子进程,但是父子进程共享进程地址空间

基于轻量级进程的系统调用,有原生线程库在用户层模拟出一套线程接口。这个原生线程库叫做pthread库。

1.2 线程优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 线程之间的切换需要操作系统的做的工作少很多,要比切换一个进程简单
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束时,可以执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
    • 计算密集型:执行流的大部分任务主要以计算为主
  • I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作
    • IO密集型:执行流的大部分任务主要是以IO为主

1.3 线程的缺点

  • 性能损失
  • 健壮性降低
    • 如果一个线程访问了全局资源会影响其他资源,导致线程不安全。一个进程中的任何一个线程崩溃了,其他所有的线程全部崩溃。
  • 缺乏访问控制
    • 进程是访问控制的基本单位,线程对函数的访问缺乏控制
  • 编程难度提高
    • 编写和调试多线程程序比单线程程序困难

1.4 线程异常

  • 单个线程如果出现除零,访问野指针,越界等问题导致线程线程崩溃的话,进程也会随着崩溃
  • 线程是进程的执行分支,如果线程出现异常,触发了信号机制的话,就会终止进程,回收进程中的所有资源

1.5 Linux进程和线程

  • 进程是资源分配的基本实体
  • 线程是调度的基本实体
  • 线程共享进程数据,但是也拥有自己的一部分数据
    • 线程ID
    • 一组寄存器(线程可以被调度)
    • 栈(线程会产生自己的数据)
    • errno
    • 信号屏蔽字
    • 调度优先级

进程的多个线程共享

多线程共享如下:

  • 同一个地址空间
    • 因此代码区和数据区都是共享的。如果定义一个函数,各线程都可以调用;如果有一个全局变量,各线程也都可以访问。此外
  • 文件描述符表
  • 每种信号的处理方式
  • 当前工作目录
  • 用户id和组id

常见的线程和进程关系:

![[Pasted image 20220210094404.png]]

2 Linux线程控制

2.1 POSIX线程库

上文说过,Linux下没有真正意义上的线程库,所以Linux使用引进的第三方库pthread库来控制线程。

  • POSIX线程库与线程有关的函数构成一个完整的系列,对绝大多数的函数都是以pthread_打头的
  • 需要引进头文件<pthreah.h>
  • 链接这些线程函数库时,要是用编译器命令的-lpthread选项包含第三方线程库

2.2 线程创建

#include <pthread.h>

int pthread_create(pthread_t* thread, const phtread_attr_t *attr, void*(start_routine)(void*), void* arg);
  • 作用
    • 创建一个新的线程,可以指定新的线程需要执行的任务
  • 参数
    • thread:输出型参数,可以返回线程的线程号
    • attr:设置线程的属性
    • start_routine:需要指定线程需要执行的任务
    • arg:给线程执行的任务传入的参数
  • 返回值
    • 成功放回0,失败返回错误码

错误检查

  • 传统的函数都是成功返回0,失败返回-1,并且对全局errno赋值表示错误
  • pthread函数出错不会设置全局的errno(大部分POSIX函数都是这样),而是将错误码直接返回
  • 每个线程也都有自己的errno来支持其他需要支持设置errno的函数,但是读取返回值要比读取errno的开销小

示例:
创建一个线程,每隔一秒打印自己的pidppid,同时主线程也每隔一秒打印自己的pidppid

注意:在编译文件的时候,需要gcc -lpthread带上链接pthread库的选项

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* Routine(void* arg)
{
   
	char* msg = (char*)arg;
	while (1)
	{
   
		printf("%s: pid: %d ppid: %d\n",msg,  getpid(), getppid());
		sleep(1);
	}
	return NULL;
}

int main()
{
   
	pthread_t tid;
	pthread_create(&tid, NULL, Routine, (void*)"thread 1");
	while (1)
	{
   
		printf("main thread: pid: %d ppid: %d\n", getpid(), getppid());
		sleep(1);
	}
	return 0;
}

由实验可知:

  1. ![[Pasted image 20220210101443.png]]
    通过试验可以看出,创建的新线程和主线程的pidppid都是相同的,因为可以确定线程是在进程中运行的

  2. 或者使用ps命令查看线程,因为查看的是线程,所以需要带上-L选项。

    1. 执行ps -aL | head -1 && ps -aL | grep 文件名![[Pasted image 20220210101809.png]]
    2. 其中LWP(light weight process)是轻量级进程的ID。可知其实操作系统调度的时候,访问的是LWP而不是PID
    3. Linux中,应用层的线程与内核的LWP是一一对应的,可以通过pthread库来操作应用层的线程,而LWP会随之产生

2.3 线程标识

#include <pthread.h>
pthread_t pthread_self(void);
  • 作用
    • 返回原生线程库提供的用户级线程ID,与pthread_create第一个输出型参数的值是一样的

示例:
创建3个线程,分别在main执行流中和线程执行任务的执行流中打印线程的ID

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

void* Routine(void* arg)
{
   
	char* msg = (char*)arg;
	// 使用pthread_self()表示线程ID
	printf("%s, ID:%x\n", msg, pthread_self());
	
	return NULL;
}

int main()
{
   
	pthread_t tid[3];
	for (int i = 0; i < 3; i ++)
	{
   
		// 将不同的进程写入buffer中
		char* buff = (char*)malloc(24);
		sprintf(buff, "thread %d", i);
		// 创建进程
		pthread_create(&tid[i], NULL, Routine, buff);
		// 使用tid表示线程ID
		printf("%s, ID: %x\n", tid[i]);
	}
	// 让主线程不要退出
	while (1)
	{
   
		sleep(1);
	}
	return 0;
}

注意这里的pthread_self()和输出型参数tid都是用户级别原生线程库提供的表示线程的ID,而使用ps -L查看的LWP内核级别表示线程的ID

用户级线程ID和内核级线程ID的联系是什么?即thread_tLWP的联系是什么?

用户级线程ID就是进程地址空间中的一个地址。

在Linux中有很多的进程,所以也就有很多的线程。而Linux中没有真正意义上的线程而是使用进程模拟出来,所以Linux系统内也就没有专门的数据结构来组织和管理这么多的线程。在Linux中操作系统只需要对LWP内核执行流进行管理。

而上面我们使用pthread库中的函数来操作的线程是pthread库中模拟出来的线程并且pthread库自己去管理并组织的线程。

pthread库是一个第三方动态库,也就是这个库本身就是一个文件。当程序运行的时候,文件加载到内存然后通过页表映射到虚拟地址的堆栈中间的共享区中。当动态库加载到共享区中并运行的时候,动态库中有一套完整地管理和组织线程的逻辑。

![[Pasted image 20220210152118.png]]

因此其实我们操作的线程是第三库模拟出的线程,我们操作也是模拟线程的从生到死。第三库库中的线程最后只需要将数据和代码交给操作系统管理的LWP执行流即可。

而因为动态库是在虚拟地址用户空间的,调度线程不需要进入内核区,只需要在用户区完成,所以线程的ID叫做用户级线程ID,而这个ID其实就是组织线程结构体的起始地址的首地址而已

所以说用户级线程ID就是进程地址空间中的一个地址(虚拟地址)

线程终止

线程终止有三种方法:

  1. 在线程执行任务的routinereturn表示线程退出
    1. main函数中return表示整个进程退出
    2. 使用exit()函数在任何地方调用表示整个进程退出
  2. pthread_exit
#include <pthread.h>
void pthread_exit(void* retval);
  • 作用
    • 退出一个线程,并且可以执行线程的退出码,作用和return效果一样

需要注意pthread_exit或者return返回的指针指向的内存单元一定要是全局的或者是malloc出来的,而不能是函数栈上分配的,因为当其他线程得到这个返回指针时,线程函数已经退出了。

  1. pthread_cancel
#include <pthread.h>
int pthread_cancel(pthread_t thread);
  • 作用
    • 指定取消一个线程号为thread的线程,相当于终止一个线程。一般用在main线程取消指定的线程,导致线程执行的过程中突然被取消。如果取消成功,线程的退出码默认就是-1, 取消失败退出码默认是0
  • 参数
    • 需要被终止的线程的线程号
  • 返回值
    • 取消成功返回0, 失败返回非0

示例:
创建3个线程,让主线程去主动取消1号2号线程,最后在main中回收这3个线程,

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

void* Routine(void*arg)
{
   
    printf("thread has created\n");
	// 线程退出码为666
	return (void*)666;
}

int main()
{
   
    pthread_t tid[3];
    for (int i = 0; i < 3; i ++) 
    {
      
        // 创建进程
        pthread_create(&tid[i], NULL, Routine, NULL);
    }  
	
	// 取消线程
	pthread_cancel(tid[1
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hyzhang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值