【Linux】线程互斥,2024年最新开发岗面试自我介绍

unsigned int __nusers;
extension union
{
struct
{
short __espins;
short __elision;
#define __spins __elision_data.__espins
#define __elision __elision_data.__elision
#define __PTHREAD_SPINS
{
0, 0
}
} __elision_data;
__pthread_slist_t __list;
};
#endif
} __data;
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;

mutex的含义

在这里插入图片描述

初始化互斥量有两种方法:

方法1,静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER。使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
方法2,动态分配:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);.

互斥锁的基本使用方法

  1. 定义互斥锁变量:
    在使用互斥锁之前,先定义一个互斥锁变量。使用pthread_mutex_t类型来声明互斥锁变量,如:pthread_mutex_t mutex;
  2. 初始化互斥锁:在使用互斥锁之前,需要对互斥锁进行初始化。
    静态初始化:在定义互斥锁变量时,使用PTHREAD_MUTEX_INITIALIZER宏进行初始化。
    动态初始化:可以使用pthread_mutex_init函数来初始化互斥锁,例如pthread_mutex_init(&mutex, NULL);第一个参数是要初始化的互斥锁变量,第二个参数是互斥锁的属性,通常使用NULL表示使用默认属性;
  3. 对临界区加锁:在访问共享资源之前,需要加锁。使用pthread_mutex_lock函数来加锁,如:pthread_mutex_lock(&mutex);如果互斥锁已经被其他线程锁定,那么当前线程会被阻塞,直到互斥锁被解锁。
  4. 访问共享资源:在互斥锁被锁定的情况下,可以安全地串行访问共享资源。
  5. 解锁:在访问共享资源完成后,需要解锁互斥锁,以便其他线程可以继续访问共享资源。使用pthread_mutex_unlock函数来解锁,例如:pthread_mutex_unlock(&mutex);
  6. 销毁互斥锁:
    不再需要使用互斥锁时,需要将其销毁。使用pthread_mutex_destroy函数来销毁互斥锁,如:pthread_mutex_destroy(&mutex);
    静态初始化的互斥锁在程序结束时会自动被系统回收,无需手动销毁。
    不要销毁一个已经加锁的互斥量
    对于已经销毁的互斥量,要确保后面不会有线程再尝试加锁

3.pthread_mutex_lock()

在这里插入图片描述
在这里插入图片描述
在访问共享资源完成后,需要解锁互斥锁。否则,其他线程会被一直阻塞。需要特别注意break, goto等跳转语句跳过解锁函数。

被互斥锁锁定的临界区只能串行执行(互斥访问),虽然保证了多执行流访问临界资源的安全性,但是会在一定程度上降低程序的效率。

尽量保证被互斥锁锁定的代码都是访问临界资源的代码,不要将其他无关的操作也放入临界区中。因为相比并发或并行执行,临界区串行执行的效率较低。

调用pthread_ lock 时,可能会遇到以下情况

  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  2. 其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

4.time() or gettimeofday

在这里插入图片描述
在这里插入图片描述

详细解释

在 Linux 下,time() 和 gettimeofday() 函数都被用于获取当前的时间或时间戳,但它们之间有一些重要的区别。

time() 函数:
time() 函数返回从 Epoch(1970年1月1日 00:00:00 UTC)到当前时间的秒数。它的精度是秒级的。

函数原型:

c
#include <time.h>
time_t time(time_t *tloc);

如果 tloc 不是 NULL,则该函数也会将结果存储在 tloc 指向的内存位置。
如果成功,它返回自 Epoch 以来经过的秒数。
如果出错,它返回 -1。
示例:

c
#include <stdio.h>
#include <time.h>

int main() {
time_t current_time;
current_time = time(NULL);
if (current_time == -1) {
perror(“time”);
return 1;
}
printf(“Current time since the Epoch: %ld\n”, (long) current_time);
return 0;
}

gettimeofday() 函数:
gettimeofday() 函数返回当前的时间,包括秒和微秒部分。因此,它的精度更高,可以达到微秒级。

函数原型:

c
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);

tv 是一个指向 timeval 结构的指针,该结构用于存储秒和微秒。
tz 是一个指向 timezone 结构的指针,但在现代系统中,它通常被设置为 NULL,因为时区信息可以通过其他方式获得。
如果成功,它返回 0。
如果出错,它返回 -1。
timeval 结构定义如下:

c
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};

示例:

c
#include <stdio.h>
#include <sys/time.h>

int main() {
struct timeval tv;
if (gettimeofday(&tv, NULL) == -1) {
perror(“gettimeofday”);
return 1;
}
printf(“Current time: %ld.%06ld\n”, (long)tv.tv_sec, (long)tv.tv_usec);
return 0;
}

注意:

由于 gettimeofday() 使用了微秒级的精度,因此在某些系统上可能比其他时间函数(如 time())更慢。
在许多现代应用中,建议使用更高级的时间处理函数,如 clock_gettime(),它提供了更多的时钟选项和更高的精度。
在多线程环境中,time() 和 gettimeofday() 都不是线程安全的。如果需要线程安全的时间函数,应考虑使用 clock_gettime() 或其他同步机制。

5.锁的相关问题

linux下,mutex的工作原理是什么?它是如何保护临界区的?

在Linux下,互斥锁(mutex)是一种同步机制,用于确保在任何给定时刻只有一个线程或进程可以进入临界区,从而避免数据竞态条件和不一致性。以下是关于mutex工作原理、如何保护临界区以及如何让线程变安全的详细解释:

互斥锁的工作原理:

互斥锁基于原子操作和硬件支持来实现其同步机制。原子操作是一种不可分割的操作,即使在多核处理器上也是如此。Linux内核利用现代处理器提供的硬件指令(如“比较并交换”CMPXCHG指令)来支持原子操作,确保在执行操作期间不会被中断或其他线程干扰。

互斥锁内部有一个计数器(通常称为互斥量),其值只能为0或1。当线程需要进入临界区时,它会检查互斥锁的状态。如果计数器的值为0,表示当前没有线程占用临界区,该线程可以获取互斥锁并进入临界区。如果计数器的值为1,则表示临界区已被其他线程占用,该线程将被阻塞,直到互斥锁被释放。

保护临界区:

临界区是访问临界资源的代码段,这些资源一次只允许一个线程或进程访问。通过使用互斥锁,可以确保在任何时候只有一个线程能够进入临界区并执行相关操作。当线程进入临界区之前,它会尝试获取互斥锁。如果成功获取到锁,线程将执行临界区内的代码;如果锁已被其他线程占用,则当前线程将被阻塞,直到锁被释放。这种方式有效地保护了临界区,避免了多个线程同时访问临界资源导致的竞态条件。

线程安全:

线程安全是指多个线程访问某个方法或资源时,不管这些线程如何交替执行,每次运行的结果都与单线程运行的结果一致。线程安全问题通常由全局变量和静态变量引起。当多个线程同时对全局变量或静态变量进行写操作时,就可能导致数据不一致或其他问题。

通过使用互斥锁,可以确保对临界资源的互斥访问,从而实现线程安全。当线程需要访问临界资源时,它必须先获取互斥锁。这确保了在同一时间只有一个线程能够访问临界资源,从而避免了数据竞态条件。一旦线程完成对临界资源的访问,它会释放互斥锁,允许其他线程进入临界区。通过这种方式,互斥锁有效地保证了线程安全。

总之,Linux下的互斥锁通过原子操作、硬件支持和内核态调用实现其工作原理,保护临界区并确保线程安全。它是多线程编程中重要的同步机制之一,用于解决多线程环境中的竞态条件和数据不一致性问题。

加锁就是串行执行了吗?

是的!执行临界区代码一定是串行的!

结合Linux下的线程互斥与同步,简要介绍什么叫做原子性

在Linux下,线程互斥与同步是并发编程中的关键概念,它们共同确保多个线程在访问共享资源时的正确性和一致性。而原子性(atomicity)则是这些概念中的一个重要属性。

原子性指的是一个操作或者一组操作要么全部执行并且执行过程不会被任何因素打断,要么就全部不执行。换句话说,一个原子操作是不可中断的,即在执行完毕之前,不会被其他线程或进程干扰。在Linux下,原子操作通常是由硬件指令和内核支持来保证的,确保在执行期间不会被其他线程打断。

在线程互斥的上下文中,原子性尤为重要。例如,当多个线程试图同时修改一个共享变量时,如果没有原子性的保证,就可能导致数据的不一致和竞态条件。通过使用互斥锁(mutex)等同步机制,可以确保对共享资源的访问是原子的,即一次只有一个线程可以访问和修改共享资源。

同样,线程同步也需要原子性的保证。条件变量、信号量等同步机制都是基于原子操作来实现的,以确保线程在同步过程中的正确性和一致性。

因此,原子性是Linux下线程互斥与同步的重要基础,它确保了多线程环境中对共享资源的正确访问和操作,从而避免了数据竞态条件和不一致性的问题。

Linux下的线程互斥与同步的原子性体现在哪里?

Linux下的线程互斥与同步的原子性主要体现在对共享资源的访问和操作的不可分割性上。原子性确保了在多线程环境中,对共享资源的访问和操作不会被其他线程打断,从而避免了数据竞态条件和不一致性的问题。

在Linux下,线程互斥与同步的原子性主要通过以下方式实现

硬件指令支持: 现代处理器提供了原子操作的硬件指令,如“比较并交换”(Compare-and-Swap,CAS)或“加载链接/存储条件”(Load-Linked/Store-Conditional)等。这些指令允许一个操作在不被中断的情况下完成,即使在多核处理器上也是如此。Linux内核利用这些硬件指令来实现原子操作,从而确保线程互斥与同步的原子性。

互斥锁(Mutex:互斥锁是一种常用的同步机制,用于保护临界区。当一个线程尝试获取互斥锁以访问临界区时,该操作是原子的。如果锁已被其他线程持有,那么获取锁的操作会失败,并且当前线程会被阻塞,直到锁被释放。这种原子性的获取和释放锁的操作确保了只有一个线程能够进入临界区,从而避免了数据竞态条件。

原子变量操作:Linux提供了原子变量操作的API,如__sync_fetch_and_add、__sync_bool_compare_and_swap等。这些函数提供了对变量进行原子性读写和修改的能力,确保了对共享变量的访问和修改不会被其他线程打断。

条件变量和信号量:条件变量和信号量也是用于线程同步的机制。条件变量允许线程在特定条件不满足时阻塞,并在条件满足时被唤醒。信号量则用于控制对共享资源的访问数量。这些机制的实现也依赖于原子操作来确保同步的正确性。

综上所述,Linux下的线程互斥与同步的原子性是通过硬件指令支持、互斥锁、原子变量操作以及条件变量和信号量等机制共同实现的。这些机制确保了多线程环境中对共享资源的访问和操作的不可分割性,从而避免了数据竞态条件和不一致性的问题。

加锁了之后,线程在临界区中,是否会切换,会有问题吗?

加锁了之后,线程在临界区中会切换,不会有问题
虽然被切换了,但是在持有锁的状态下被切换的
所以其他抢票线程要执行临界区代码,也必须先申请锁
但是其他线程是无法申请成功的,所以其他线程无法进入临界区,保证了临界区中数据一致性
一个访问临界资源的线程不申请锁 当其他持有锁的线程被切换后 他依然可以访问临界资源 – 错误的编码方式,程序员就不要写出这样的代码,即程序员要给所有访问临界资源的线程的加锁。
对于访问临界资源的线程而言,临界区代码要么全部执行成功,要么全部不执行,访问临界资源的操作不可被中断(不能同时执行其他线程的临界区代码),这就是原子性的体现

对没有持有锁的线程2最有意义的情况有两种:

  1. 线程1没有持有锁 不访问临界资源
  2. 线程1释放锁 线程2可以申请锁

线程要访问临界资源 必须先申请锁 前提:每一个线程都必须先看到同一把锁
==> 锁本身是不是就是一种共享资源?谁来保证锁的安全呢?
为了保证锁的安全,申请和释放锁必须是 原子的!如何保证?锁是如何实现的?

6.锁的原理

  1. 在汇编层面,一条汇编语句要么已经执行完,要么就还没有执行,是原子性的。
  2. 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange汇编指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。即使是多处理器平台(并行),访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
  3. CPU的寄存器数据,本质就是当前执行流的上下文。寄存器的存储空间被所有执行流共享,但是寄存器的内容是被每一个执行流私有的【意思就是,所有的执行流都能将自己的上下文放到cpu寄存器内,让寄存器来调度,但是同一时间,cpu的寄存器只能被一个执行流私有,即只能有一个执行流被调度】所以在切换线程时,要将当前线程的寄存器数据保存到其PCB中,并加载要调度的新线程的寄存器数据。
  4. swap/exchange指令:以一条汇编的方式,将内存和CPU内寄存器数据进行交换
  5. 在汇编的角度,如果一个操作只用一条汇编语句就可以完成,称该操作的执行是原子的!【本章的第一部分(计算机如何完成y = a * b + c ?)】实际上讲解了更底层指令的过程。
  6. 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。

执行流视角是如何看待CPU寄存器的?

CPU内部的寄存器本质,叫做当前执行流的上下文!寄存器空间是被所有的执行流共享的,但是寄存器的内容,是被每一个执行流私有的!

图解互斥锁

在这里插入图片描述

假设共识:

  1. 将互斥锁变量mutex理解成一个整形变量,某进程cpu上下文寄存器的值为1表示互斥锁被线程持有。在内存创建互斥锁变量初始化为1。
  2. 由于exchange汇编指令是原子的,所以不管线程如何切换,只有一个线程能够将mutex(内存)中的1值交换到自己的寄存器当中,即该线程的上下文中。而线程上下文是线程的私有数据,实现了公有到私有的转换。同时寄存器当中的0值被交换到了mutex中,其他线程再进行交换也只能交换到0。
  3. 在进行if判断时,交换到1值的线程执行return 0,可以安全地进入临界区访问临界资源;而交换到0值的线程阻塞等待,直到互斥锁被解锁,这些线程才会被唤醒,然后再次尝试申请锁
  4. 当持有锁的线程访问完临界资源后,会将mutex变量重新置为1,即解锁互斥锁。之后OS唤醒等待互斥锁解锁的线程,让他们再次竞争申请锁。
  5. 为了保证锁的安全,申请和释放锁,必须是原子的!在设计加锁时,通过一条原子性的exchange指令,保证了加锁和解锁的原子性。
  6. 加锁了之后,线程在临界区中,是否会切换,会有问题吗?线程在临界区中也可能会被切换,但他是持有锁被切换的,所谓持有锁切换是指互斥锁的1值保存在当前线程的上下文,被当前线程私有。而其他线程即使被CPU调度执行,也无法抢占互斥锁,也就无法访问临界区代码。所以不会有任何问题。

假设存在的情况:

A执行1:cpu寄存器值=0;执行2前被切换;A带着CPU寄存器的值0回老家【保存上下文】
B执行1:cpu寄存器值=0;执行2,交换使得mutex=0,寄存器=1;执行3,if条件满足,进入if,执行4前被切换;B带着CPU寄存器的值1回老家【保存上下文】
A继续执行2,交换使得mutex=0,寄存器=1;执行3,if条件不满足,挂起等待。
B继续执行4,return 0;表示加锁成功。
A等待结束,执行6,进行A的加锁。
如此,mutex自己保证了自己的原子性。那个“1”就好像一个令牌,不管有多少个线程,令牌只有一个,线程即使被切换,他也是带着令牌走的。

有同学会问,我们为了使得一个全局变量ticket被安全的访问,又添加了一个mutex,现如今为了保护ticket搞了一个也要被保护的mutex,为什么不直接将保护mutex的思想用到ticket上?这是多此一举吗?

当然不是。在多线程编程中,多个地方都会用到锁,如果我们在项目中编写代码时,加一个锁就把对应的代码添加一些if/else/exchange,这样的代码质量简直难评。设计者给我们设计了一个线程库,设计者考虑到线程安全的问题,设计了锁这样的概念,为的就是让程序员使用时,能够简单通过加锁/解锁这样的语句实现复杂的原子操作,类似于封装有益于提高代码可维护性可重用性的思想。

7.可重入VS线程安全

线程安全:

多个线程并发执行时,在没有锁保护的情况下访问了共享资源(如全局或静态变量,堆区数据等),会出现数据竞争从而导致数据冲突,数据不一致等线程安全问题。多个线程并发执行同一段代码时,不会出现不同的结果称之为线程安全。常见的如果对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程不安全的问题。

重入:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
img

给大家的福利

零基础入门

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:

在这里插入图片描述

因篇幅有限,仅展示部分资料

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

]

给大家的福利

零基础入门

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:

在这里插入图片描述

因篇幅有限,仅展示部分资料

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

  • 28
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值