【原理剖析】8-Linux 线程概述

概述:知识点

  • Linux 线程的基本概念
  • Linux 共享内存相关 API 介绍

0x01 Linux 线程的基本概念

Linux 操作系统引入线程主要是为了提高系统的执行效率,减少处理机的空转时间和在进行调度切换时因保护现场信息所用的时间,便于系统管理。

线程(Threads)是进程中执行运算的最小单位,即执行处理机调度的基本代为。在 Linux 操作系统中,可以在一个进程内部进行线程切换,现场保护工作量小,并且通过共享线程的基本资源可以减轻系统开销。此外,进程中线程的切换也能提高现场切换的效率。因此,一个进程内的基本调度单位称为线程或轻型线程,这个调度单位既可以由操作系统内核控制,也可以由用户程序控制。下面就将前面所学到的进程与线程进行比较来进一步理解线程的概念:

  • 进程是操作系统资源分配的基本单位。所有与该进程有关的资源,如外部设备、缓冲区队列等,都被记录在进程控制块 PCB 中,以表示该进程拥有这些资源。同一进程的所有线程共享该进程的所有资源。
  • 线程是分配处理机的基本单位。它与资源分配无关,即真正在处理机上运行的是线程。
  • 一个线程只能属于一个进程,而一个进程可以拥有多个线程,但至少有一个线程。
  • 线程在执行过程中需要协作同步,不同进程的线程间要利用消息通信的方式实现同步(进程间通信)。

虽然上面列出了很多线程与进程的区别,但是在 Linux 操作系统中并没有进行太多区分,对进程和线程都用了相同的描述方法以及相同的调度和管理策略。

0x02 Linux 线程的状态

Linux 操作系统中,线程与进程一样,也有自己的状态。线程有三种基本状态,即执行、阻塞和就绪,没有进程状态中的挂起状态。因此,线程是一个只与内存和寄存器相关的概念,内容不会因为交换而进入内存。

针对线程的三种状态,系统中存在五种基本操作来转换线程状态:

  • 派生:线程在进程中派生出来,也可再派生线程。用户可以通过相关的系统调用派生自己的线程。在 Linux 系统中,库函数 clone()creat_thread() 分别用来派生不同执行模式下的线程。新派生的线程具有相应的数据结构指针和变量,这些指针和变量作为寄存器上下文放在本线程的寄存器和堆栈中。新派生的线程被放入就绪队列。
  • 调度:选择一个就绪线程进入执行状态。
  • 阻塞:像进程一样,如果一个线程在执行过程中需要等待某个事件发生,则被阻塞,阻塞时,寄存器上下文、程序计数器以及堆栈指针都会得到保存。
  • 激活:如果阻塞线程所等待的事件发生,则该线程被激活并进入就绪队列。
  • 结束:如果一个线程执行结束,它的寄存器上下文以及堆栈内容等将被释放。

0x03 引入线程的优势

多线程机制是指操作系统支持在一个进程内自行多个线程的能力。从线程的观点分析,MS-DOS 仅支持一个用户进程和一个线程;UNIX 系统支持多个用户进程,当一个进程只能有一个线程;WindowsNTSolarisLinux 等支持多进程多线程。

虽然多种系统都支持多线程,但是实现的方式也不相同。线程有两个基本类型:

  • 用户级线程
  • 系统级线程(核心级线程)
  1. 用户级线程 用户级线程简称为 ULT ,由用户应用程序建立的,并由用户应用程序负责对这些线程进行调度和管理,操作系统内核并不知道有用户级线程的存在,只对进程进行管理。因此这种线程与内核无关。MS-DOSUNIX 操作系统下线程就属于这种。

这种纯 ULT 方法的优点如下:

  • 应用程序中线程的开关的时空开箱远远小于内核级线程的开销
  • 线程的调度算法与操作系统的调度算法无关
  • 用户级线程方法适用于任何操作系统,因为与内核无关

缺点也很明显,如下:

  • 在一个典型的操作系统中,有许多系统请求正被阻塞着,因此,当线程执行一个系统请求时,不仅本线程阻塞,而且该线程所在进程中的所有线程都会被阻塞
  • 在该方法的系统中,因为每个进程每次只能由一个线程在 CPU 中运行,因此,一个线程应用无法利用多处理器的优点。
  1. 内核级线程

内核级线程简称为 KLT 。内核级线程中所有线程的创建、调度和管理全部由操作系统内核负责完成。一个应用进程可按多线程方式编写程序,当它被提交给多线程操作系统运行时,内核为它创建一个进程和一个线程,线程在运行中还会创建新的线程。操作系统内核给应用程序提供相应的系统调用和应用程序接口,以使用户程序可以创建、执行、撤销线程。这种内核级线程的优点如下:

  • 内核可以调度一个进程中的线程,使其同时在多个处理机上并行运行,从而提高系统的效率
  • 当进程中的一个线程被阻塞时,进程中的其他线程仍然可运行
  • 内核本身可以以线程方式实现

缺点也很明显,由于线程调度程序运行在内核态,而应用程序运行在用户态,因此同一个进程中的线程切换要经过用户态到内核态,再从内核态到用户态的两次模式装换。

  1. 用户级线程与核心机线程相结合的模式

由于用户级线程和内核级线程各有特点,因此,如果将两种方式相结合起来,则可以吸取两者的优点,结合起来的系统称为多线程的操作系统。内核支持多线程的建立、调度和管理。同时系统中又提供使用线程库,允许用户应用程序建立、调度和管理用户级线程。

Linux 的内核级线程和其他操作系统的内核实现不同。大多数操作系统单独定义描述线程的数据结构,采用单独的线程管理方式,提供专门的线程调度,这些都增加了内核和调度程序的复杂性。而在 Linux 中,将线程定义为“执行上下文”,它实际上只是进程的另外执行上下文而已,和进程采用同样的表示、管理、调度方式。这样,Linux 内核并不需要区分线程和进程,只需要一个进程/线程管理数组,而且调度程序也只有进程的调度程序,内核的实现相对简单得多,而且节约系统用于管理方面的时间开销。

0x04 Linux线程相关 API 函数

Linux 系统下的多线程遵循 POSIX 线程接口,称为 pthread ,编写 Linux 下的多线程程序需要使用头文件 pthread.h ,编译时需要使用 -pthread 链接使用库 libpthread.a,创建线程的函数接口是 pthread_create ,具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <pthread.h>

int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);

/*
参数:
thread:负责向调用者传递子线程的线程号
attr:负责控制线程的各种属性,具体会用到属性设置结构体 pthread_attr_t,如下:
typedef struct
{
int detachstate; //线程的分离状态
int schedpolicy; //线程调度策略
structsched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程堆栈的地址集
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
start_routine:这个参数负责指定子线程需要允许的函数,这个参数需要一个函数指针。
arg:参数指定子线程运行函数所需的参数值。
返回值:
成功返回 0,识别返回 -1
*/

线程退出函数 pthread_exit(void *retval) ,具体如下:

1
2
3
4
5
6
7
8
#include <pthread.h>

void pthread_exit(void *retval);

/*
参数:
retval:留给主线程回收使用的退出状态值
*/

主线程回收子线程资源函数 pthread_join(),当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。当 pthread_join() 函数返回后,被调用线程才算真正意义上的结束,内存空间才会被释放(如果被调用线程是非分离情况下),具体如下:

1
2
3
4
5
6
7
8
9
10
11
#include <pthread.h>

int pthread_join(pthread_t th, void **thread_return);

/*
参数:
th:被连接线程的线程号
thread_return:指向一个被连接线程的返回码的接收位置
返回值:
成功返回 0,失败返回一个 error number
*/

多线程程序中可能会存在数据不一致的情况,那么如何保证数据一致呢?可以考虑同一时间只有一个线程访问数据。互斥量(mutex)就是一把锁。多个线程只有一把锁一个钥匙,谁上的锁就只有谁能开锁。当一个线程要访问一个共享变量时,先用锁把变量锁住,然后再操作,操作完了之后再释放掉锁,完成。当另一个线程也要访问这个变量时,发现这个变量被锁住了,无法访问,它就会一直等待,直到锁没了,它再给这个变量上个锁,然后使用,使用完了释放锁,以此进行。这个即使有多个线程同时访问这个变量,也好象是对这个变量的操作是顺序进行的。

互斥变量使用特定的数据类型:pthread_mutex_t,使用互斥量前要先初始化,使用的函数如下:

1
2
3
4
5
6
7
8
9
10
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

//对互斥量加锁解锁的函数如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

对于线程锁的相关介绍有很多,后面有机会咱们详细介绍一下。这里先暂时了解部分用于线程同步使用。

0x05 实验一:Linux 线程创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *mythread1(void)
{
int i;
for(i = 0; i < 10; i++)
{
printf("This is the 1st pthread,created by xiaoqiang!\n");
sleep(1);
}
}

void *mythread2(void)
{
int i;
for(i = 0; i < 10; i++)
{
printf("This is the 2st pthread,created by xiaoqiang!\n");
sleep(1);
}
}

int main(int argc, const char *argv[])
{
int i = 0;
int ret = 0;
pthread_t id1,id2;

ret = pthread_create(&id1, NULL, (void *)mythread1,NULL);
if(ret)
{
printf("Create pthread error!\n");
return 1;
}

ret = pthread_create(&id2, NULL, (void *)mythread2,NULL);
if(ret)
{
printf("Create pthread error!\n");
return 1;
}

pthread_join(id1,NULL);
pthread_join(id2,NULL);

return 0;
}

0x06 实验二:Linux 线程同步问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>
#include <string.h>
#define MAX 10

pthread_t thread[2]; //两个线程
pthread_mutex_t mut;
int number=0;
int i;
void *thread1()
{
printf ("thread1 : I'm thread 1\n");

for (i = 0; i < MAX; i++) //模拟线程执行时间
{
printf("thread1 : number = %d\n",number);
pthread_mutex_lock(&mut);
number++;
pthread_mutex_unlock(&mut);
sleep(2);
}


printf("thread1 :主函数在等我完成任务吗?\n");
pthread_exit(NULL);
}

void *thread2()
{
printf("thread2 : I'm thread 2\n");

for (i = 0; i < MAX; i++)
{
printf("thread2 : number = %d\n",number);
pthread_mutex_lock(&mut);
number++;
pthread_mutex_unlock(&mut);
sleep(3);
}


printf("thread2 :主函数在等我完成任务吗?\n");
pthread_exit(NULL);
}

void thread_create(void) //创建两个线程
{
int temp;
memset(&thread, 0, sizeof(thread)); //comment1
/*创建线程*/
if((temp = pthread_create(&thread[0], NULL, thread1, NULL)) != 0) //comment2
printf("线程1创建失败!\n");
else
printf("线程1被创建\n");

if((temp = pthread_create(&thread[1], NULL, thread2, NULL)) != 0) //comment3
printf("线程2创建失败");
else
printf("线程2被创建\n");
}

void thread_wait(void)
{
/*等待线程结束*/
if(thread[0] !=0)

{ //comment4

pthread_join(thread[0],NULL);
printf("线程1已经结束\n");
}
if(thread[1] !=0)

{

//comment5

pthread_join(thread[1],NULL);
printf("线程2已经结束\n");
}
}

int main()
{
/*用默认属性初始化互斥锁*/
pthread_mutex_init(&mut,NULL);

printf("我是主函数哦,我正在创建线程,呵呵\n");
thread_create();
printf("我是主函数哦,我正在等待线程完成任务阿,呵呵\n");
thread_wait();

return 0;
}

【原理剖析】8-Linux 线程概述
https://hodlyounger.github.io/A_OS/Linux/Linux操作系统原理剖析/【原理剖析】8-Linux 线程概述/
作者
mingming
发布于
2023年12月27日
许可协议