2024年运维最新Linux 多线程原理深剖_线程的布局(1),2024年最新Linux运维编程基础教程

最全的Linux教程,Linux从入门到精通

======================

  1. linux从入门到精通(第2版)

  2. Linux系统移植

  3. Linux驱动开发入门与实战

  4. LINUX 系统移植 第2版

  5. Linux开源网络全栈详解 从DPDK到OpenFlow

华为18级工程师呕心沥血撰写3000页Linux学习笔记教程

第一份《Linux从入门到精通》466页

====================

内容简介

====

本书是获得了很多读者好评的Linux经典畅销书**《Linux从入门到精通》的第2版**。本书第1版出版后曾经多次印刷,并被51CTO读书频道评为“最受读者喜爱的原创IT技术图书奖”。本书第﹖版以最新的Ubuntu 12.04为版本,循序渐进地向读者介绍了Linux 的基础应用、系统管理、网络应用、娱乐和办公、程序开发、服务器配置、系统安全等。本书附带1张光盘,内容为本书配套多媒体教学视频。另外,本书还为读者提供了大量的Linux学习资料和Ubuntu安装镜像文件,供读者免费下载。

华为18级工程师呕心沥血撰写3000页Linux学习笔记教程

本书适合广大Linux初中级用户、开源软件爱好者和大专院校的学生阅读,同时也非常适合准备从事Linux平台开发的各类人员。

需要《Linux入门到精通》、《linux系统移植》、《Linux驱动开发入门实战》、《Linux开源网络全栈》电子书籍及教程的工程师朋友们劳烦您转发+评论

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

MMU 这个硬件完成的,该硬件集成在CPU内。页表是一种软件映射,MMU是一种硬件映射,所以计算机进行虚拟地址到物理地址的转化采用的是软硬件结合的方式

在 Linux 中,32位平台下用的是二级页表,而64位平台下用的是多级页表。这就可以解释一下为什么修改字符串常量会出发段错误:我们修改一个常量虚拟必须找到对应的物理地址,但是查表时发现它是只读的,此时就会在 MMU 触发硬件错误,操作系统识别到报错后,就会发送信号对齐进行终止。

线程优点🤔

  1. 创建一个线程的代价远比一个进程小的多
  2. 相比进程之间的切换,线程之间的切换所需要的系统操作会少很多
  3. 线程占的资源比进程少得多
  4. 充分利用处理器的可并行数量
  5. 在等待慢速IO结束的同时,程序可执行其他的计算任务
  6. 计算密集型任务,在多处理器上运行,会分成多个线程运行
  7. IO密集行任务,将IO操作重修,可提高效率,线程可以同时等待不同的IO操作

线程缺点🤔

  1. 性能损失,如果计算密集型线程的任务比可用的处理器多,会产生较大的性能损失,性能包括额外的同步和调度开销,但是可用资源是不变的
  2. 健壮性降低,多线程需要全面的思考,因时间分配上的细微偏差或者因共享了不该共享的变量会造成不良影响,也就是说线程之间是缺乏保护的
  3. 缺乏访问控制,进程是访问控制的基本粒度,线程中调用 os 函数会对整个进程产生影响
  4. 难度提高,调试和编写都涉及更复杂

线程异常🤔

比如一个线程出现除0,野指针问题,会直接全部垮掉;且线程是进程的执行分支名,线程出现异常,就像进程出现异常一样,会触发信号机制,此时所有的线程会全部退出。因此合理使用多线程,能提高 CPU 密集型程序的执行效率与用户体验

进程与线程🤔

进程是分配系统资源的基本单位,线程是系统调度的基本单位
在这里插入图片描述

线程共享进程数据,但也拥有自己的一部分数据,比如线程ID,线程栈(每个线程都有临时的数据,需要压栈出栈),全局变量 errno,信号屏蔽字与调度优先级

多线程共享🤔

同一个地址空间里面,代码段和数据段和数据段都是共享的,如果定义一个函数在各线程中都可以调用。如果定义一个全局变量,在各线程中都可以访问到

各线程还共享一些进程资源和环境,文件描述符表(进程打开一个文件后,其他线程也能够看到)每种信号的处理方式,用户ID和组ID

Linux线程控制🤔

POSIX线程库😋

pthread线程库是应用层的原生线程库:

应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方帮我们提供的。原生指的是大部分Linux系统都会默认带上该线程库。
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的,要使用这些函数库,就要引入头文件<pthreaad.h>,链接这些线程函数库时,要使用编译器命令的“-lpthread”选项

传统的一些函数是,成功返回0,失败返回-1,并且对全局变量 errno 赋值以指示错误。pthreads 出错时不会设置全局变量 errno,因为大部分POSIX函数会这样做,而是将错误代码通过返回值返回。
pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于 pthreads 的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的 errno 变量的开销更小

线程的创建😋

创建线程用到 pthread_create 函数:

int pthread\_create(pthread_t \*thread, const pthread_attr_t \*attr, void \*(\*start_routine) (void \*), void \*arg);

thread:获取创建成功的线程ID,该参数是一个输出型参数
attr:用于设置创建线程的属性,传入NULL表示使用默认属性
start_routine:该参数是一个函数地址,表示线程例程,即线程启动后要执行的函数。
arg:传给线程例程的参数

当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,这个线程就叫做主线程,主线程产生其他子线程,主线程一般最后要完成某些操作,比如各种关闭动作

主线程调用 pthread_create 创建一个新线程,此后新线程会跑去执行自己的代码,而主线程则继续执行后续代码:

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

void\* Routine(void\* arg)
{
	char\* msg = (char\*)arg;
	while (1){
		printf("I am %s\n", msg);
		sleep(1);
	}
}
int main()
{
	pthread_t tid;
	pthread\_create(&tid, NULL, Routine, (void\*)"thread 1");
	while (1){
		printf("I am main thread!\n");
		sleep(2);
	}
	return 0;
}

运行代码后,可以看到新线程每隔一秒就会执行一次打印操作,但是主线程每隔两秒执行一次打印操作:

在这里插入图片描述
使用ps -aL命令可查看当前的轻量级进程(不带 -L 是一个个的进程;带 -L 是每个进程内的多个轻量级进程)

在这里插入图片描述
这里的 LWP 就是轻量级进程 ID,可以看到显示的两个轻量级进程的 PID 相同,因为它们属于同一个进程。

注意

\color{red} {注意}

注意在应用层现场其实就是 LWP,操作系统实际调度的是 LWP 而并非 PID,之前因为我一直是作为单线程进程在写,所以 PID 和 LWP 是相等的,所以单线程下调度谁都可以

上面是主线程创建一个新线程,我们让主线程一次创建多个新线程,并让每一个新线程都去执行同一个函数,也就是说该函数是会被重入的:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void\* Routine(void\* arg)
{
	char\* msg = (char\*)arg;
	while (1){
		printf("I am %s...pid: %d, ppid: %d\n", msg, getpid(), getppid());
		sleep(1);
	}
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++){
		char\* buffer = (char\*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread\_create(&tid[i], NULL, Routine, buffer);
	}
	while (1){
		printf("I am main thread...pid: %d, ppid: %d\n", getpid(), getppid());
		sleep(2);
	}
	return 0;
}

这里就可以看到 5 个创建成功的线程:

在这里插入图片描述
因为都属于同一个进程,所以他们的 PID 和 PPID 也都是一样的,此时使用 ps -aL 就能看到 6 个轻量级进程:

在这里插入图片描述

获取线程id😎

我们可以通过 pthread_self 函数获取线程 id,类似于 getpid 获取当前进程ID:

pthread_t pthread\_self(void);

下面代码中在每一个新线程被创建后,主线程都将通过输出型参数获取到的线程ID进行打印,此后主线程和新线程又通过调用 pthread_self 函数获取线程ID进行打印:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void\* Routine(void\* arg)
{
	char\* msg = (char\*)arg;
	while (1){
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread\_self());
		sleep(1);
	}
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++){
		char\* buffer = (char\*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread\_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\n", buffer, tid[i]);
	}
	while (1){
		 printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread\_self());
		 sleep(2);
	}
	return 0;
}

在这里插入图片描述
pthread_self 获得的线程ID与内核的 LWP 的值是不相等的,pthread_self 获得的是用户级原生线程库的线程ID,而 LWP 是内核的轻量级进程ID

线程等待😋

需要明确的是,一个线程被创建出来就如同进程一样,也是需要被等待的。如果主线程不对新线程进行等待,资源也是不会被回收的。所以线程需要被等待,如果不等待会产生类似于“僵尸进程”的内存泄漏问题。

等待线程函数 pthread_join:

int pthread\_join(pthread_t thread, void \*\*retval);

thread:被等待线程的ID,retval:线程退出时的退出码信息,线程等待成功返回0,失败返回错误码

retval

  1. 如果thread线程通过return返回,retval 所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用 pthread_cancel 异常终止掉,retval 所指向的单元里存放的是常数PTHREAD_CANCELED
  3. 如果thread线程是自己调用 pthread_exit 终止的,retval 所指向的单元存放的是传给 pthread_exit 的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传 NULL 给 retval 参数

grep命令进行查找,可以发现PTHREAD_CANCELED实际上就是头文件<pthread.h>里面的一个宏定义,它本质就是 -1:

grep -ER "PTHREAD\_CANCELED" /usr/include/

在这里插入图片描述
下面代码中我们先不关心线程退出信息,直接将 pthread_join 的第二次参数设置为NULL,等待线程后打印该线程的编号以及线程ID:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void\* Routine(void\* arg)
{
	char\* msg = (char\*)arg;
	int count = 0;
	while (count < 5){
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread\_self());
		sleep(1);
		count++;
	}
	return NULL;
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++){
		char\* buffer = (char\*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread\_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\n", buffer, tid[i]);
	}
	printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread\_self());
	for (int i = 0; i < 5; i++){
		pthread\_join(tid[i], NULL);
		printf("thread %d[%lu]...quit\n", i, tid[i]);
	}
	return 0;
}

在这里插入图片描述
线程退出时如何获取退出码呢?我们将线程退出时的退出码设置为某个特殊的值,并在成功等待线程后将该线程的退出码进行输出

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void\* Routine(void\* arg)
{
	char\* msg = (char\*)arg;
	int count = 0;
	while (count < 5){
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread\_self());
		sleep(1);
		count++;
	}
	return (void\*)2022;
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++){
		char\* buffer = (char\*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread\_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\n", buffer, tid[i]);
	}
	printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread\_self());
	for (int i = 0; i < 5; i++){
		void\* ret = NULL;
		pthread\_join(tid[i], &ret);
		printf("thread %d[%lu]...quit, exitcode: %d\n", i, tid[i], (int)ret);
	}
	return 0;
}

在这里插入图片描述
pthread_join 默认是以阻塞的方式进行线程等待的,那么问题来了:为什么线程退出时只能拿到线程的退出码?这是什么问题呢?比如我们可以通过 wait 或是waitpid 的输出型参数 status,获取到退出进程的退出码、退出信号以及core dump标志。就是说只能拿到退出码难道线程就不会出现异常吗?

线程当然也会出现异常,线程退出的情况有三种:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

因此我们也需要考虑线程异常终止的情况,但是 pthread_join 无法获取线程异常退出的信息。因为线程是进程内的一个执行分支,如果进程中的某个线程崩溃了,那么整个进程也会因此而崩溃,此时我们根本没办法执行 pthread_join,因为整个进程已经退出了

我们在线程的执行例程当中制造一个除零错误,当某一个线程执行到此处时就会崩溃,进而导致整个进程崩溃:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void\* Routine(void\* arg)
{
	char\* msg = (char\*)arg;
	int count = 0;
	while (count < 5){
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread\_self());
		sleep(1);
		count++;
		int a = 1 / 0; //error
	}
	return (void\*)2022;
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++){
		char\* buffer = (char\*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread\_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\n", buffer, tid[i]);
	}
	printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread\_self());
	for (int i = 0; i < 5; i++){
		void\* ret = NULL;
		pthread\_join(tid[i], &ret);
		printf("thread %d[%lu]...quit, exitcode: %d\n", i, tid[i], (int)ret);
	}
	return 0;
}

结果如下,程序直接暴毙了:

在这里插入图片描述
所以 pthread_join 只能获取到线程正常退出时的退出码,用于判断线程的运行结果是否正确。

线程终止😋

只终止某个线程而不是终止整个进程,可以有三种方法:

  1. 线程函数处进行return。
  2. 线程可以自己调用pthread_exit函数终止自己。
  3. 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程。

线程函数处进行

r

e

t

u

r

n

\color{red} {线程函数处进行return}

线程函数处进行return

注意在线程中使用return代表当前线程退出,但是在main函数中使用return代表整个进程退出

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void\* Routine(void\* arg)
{
	char\* msg = (char\*)arg;
	while (1){
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread\_self());
		sleep(1);
	}
	return (void\*)0;
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++){
		char\* buffer = (char\*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread\_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\n", buffer, tid[i]);
	}
	printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread\_self());

	return 0;
}

运行代码,并不能看到新线程执行的打印操作,因为主线程的退出导致整个进程退出了

在这里插入图片描述

p

t

h

r

e

a

d

e

x

i

t

函数

\color{red} {pthread_exit函数}

pthreade​xit函数

void pthread\_exit(void \*retval);

retval:线程退出时的退出码信息,该函数无返回值,跟进程一样结束时无法返回自身。

pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是 malloc 分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了

这里我们使用 pthread_exit 终止线程,并将退出码设置为6666:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void\* Routine(void\* arg)
{
	char\* msg = (char\*)arg;
	int count = 0;
	while (count < 5){
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread\_self());
		sleep(1);
		count++;
	}
	pthread\_exit((void\*)6666);
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++){
		char\* buffer = (char\*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread\_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\n", buffer, tid[i]);
	}
	printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread\_self());
	for (int i = 0; i < 5; i++){
		void\* ret = NULL;
		pthread\_join(tid[i], &ret);
		printf("thread %d[%lu]...quit, exitcode: %d\n", i, tid[i], (int)ret);
	}
	return 0;
}

在这里插入图片描述

p

t

h

r

e

a

d

c

a

n

c

e

l

函数

\color{red} {pthread_cancel函数}

pthreadc​ancel函数

int pthread\_cancel(pthread_t thread);


为了做好运维面试路上的助攻手,特整理了上百道 **【运维技术栈面试题集锦】** ,让你面试不慌心不跳,高薪offer怀里抱!

这次整理的面试题,**小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。**

![](https://img-blog.csdnimg.cn/img_convert/f48518cf646bef01af77feece545eec3.png)

本份面试集锦涵盖了

*   **174 道运维工程师面试题**
*   **128道k8s面试题**
*   **108道shell脚本面试题**
*   **200道Linux面试题**
*   **51道docker面试题**
*   **35道Jenkis面试题**
*   **78道MongoDB面试题**
*   **17道ansible面试题**
*   **60道dubbo面试题**
*   **53道kafka面试**
*   **18道mysql面试题**
*   **40道nginx面试题**
*   **77道redis面试题**
*   **28道zookeeper**

**总计 1000+ 道面试题, 内容 又全含金量又高**

*   **174道运维工程师面试题**

> 1、什么是运维?

> 2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

> 3、现在给你三百台服务器,你怎么对他们进行管理?

> 4、简述raid0 raid1raid5二种工作模式的工作原理及特点

> 5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

> 6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

> 7、Tomcat和Resin有什么区别,工作中你怎么选择?

> 8、什么是中间件?什么是jdk?

> 9、讲述一下Tomcat8005、8009、8080三个端口的含义?

> 10、什么叫CDN?

> 11、什么叫网站灰度发布?

> 12、简述DNS进行域名解析的过程?

> 13、RabbitMQ是什么东西?

> 14、讲一下Keepalived的工作原理?

> 15、讲述一下LVS三种模式的工作过程?

> 16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

> 17、如何重置mysql root密码?

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

**总计 1000+ 道面试题, 内容 又全含金量又高**

*   **174道运维工程师面试题**

> 1、什么是运维?

> 2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?

> 3、现在给你三百台服务器,你怎么对他们进行管理?

> 4、简述raid0 raid1raid5二种工作模式的工作原理及特点

> 5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?

> 6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?

> 7、Tomcat和Resin有什么区别,工作中你怎么选择?

> 8、什么是中间件?什么是jdk?

> 9、讲述一下Tomcat8005、8009、8080三个端口的含义?

> 10、什么叫CDN?

> 11、什么叫网站灰度发布?

> 12、简述DNS进行域名解析的过程?

> 13、RabbitMQ是什么东西?

> 14、讲一下Keepalived的工作原理?

> 15、讲述一下LVS三种模式的工作过程?

> 16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?

> 17、如何重置mysql root密码?

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值