当前位置: 首页 > news >正文

操作系统面经1——进程管理

1、用户态和内核态的区别?

内核态和用户态是操作系统中的两种运行模式。它们的主要区别在于权限和可执行的操作:


 ·内核态(KernelMode):在内核态下,CPU可以执行所有的指令和访问所有的硬件资源。这种模式下的操作具有更高的权限,主要用于操作系统内核的运行。

·用户态(User Mode):在用户态下,CPU只能执行部分指令集,无法直接访问硬件资源。这种模式下的操作权限较低,主要用于运行用户程序。
        内核态的底层操作主要包括:内存管理、进程管理、设备驱动程序控制、系统调用等。这些操作涉及到操作系统的核心功能,需要较高的权限来执行。
分为内核态和用户态的原因主要有以下几点:
        ·安全性:通过对权限的划分,用户程序无法直接访问硬件资源,从而避免了恶意程序对系统资源的破坏。
        ·稳定性:用户态程序出现问题时,不会影响到整个系统,避免了程序故障导致系统崩溃的风险。
        ·隔离性:内核态和用户态的划分使得操作系统内核与用户程序之间有了明确的边界,有利于系统的模块化和维护。
        内核态和用户态的划分有助于保证操作系统的安全性、稳定性和易维护性。

2、线程和进程的区别

.本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
·在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;
线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
.稳定性方面:进程中某个线程如果崩溃了,可能会导致整个进程都崩溃。而进程中的子进程崩溃,并不
会影响其他进程。
.内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系
统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
.包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线
的,而是多条线。
 

3、为什么进程崩溃不会对其他进程产生很大影响

主要是因为:

        ·进程隔离性:每个进程都有自己独立的内存空间,当一个进程崩溃时,其内存空间会被操作系统回收,不会影响其他进程的内存空间。这种进程间的隔离性保证了一个进程崩溃不会直接影响其他进程的执行。
        ·进程独立性:每个进程都是独立运行的,它们之间不会共享资源,如文件、网络连接等。因此,一个进程的崩溃通常不会对其他进程的资源产生影响。

4、你说到进程是分配资源的基本单位,那么这个资源指的是什么?

        虚拟内存、文件句柄、信号量等资源。

5、多线程比单线程的优势,劣势?

·多线程比单线程的优势:提高程序的运行效率,可以充分利用多核处理器的资源,同时处理多个任务,加快程序的执行速度。
·多线程比单线程的劣势:存在多线程数据竞争访问的问题,需要通过锁机制来保证线程安全,增加了加锁的开销,并且还会有死锁的风险。多线程会消耗更多系统资源,如CPU和内存,因为每个线程都需要占用一定的内存和处理时间。


6、多线程是不是越多越好,太多会有什么问题?

多线程不一定越多越好,过多的线程可能会导致一些问题。
.切换开销:线程的创建和切换会消耗系统资源,包括内存和CPU。如果创建太多线程,会占用大量的系统资源,导致系统负载过高,某个线程崩溃后,可能会导致进程崩溃。
.死锁的问题:过多的线程可能会导致竞争条件和死锁。竞争条件指的是多个线程同时访问和修改共享资源,如果没有合适的同步机制,可能会导致数据不一致或错误的结果。而死锁则是指多个线程相互等待对方释放资源,导致程序无法继续执行。

7、进程切换和线程切换的区别?

1)进程切换:进程切换涉及到更多的内容,包括整个进程的地址空间、全局变量、文件描述符等。因此,进程切换的开销通常比线程切换大。

2)线程切换:线程切换只涉及到线程的堆栈、寄存器和程序计数器等,不涉及进程级别的资源,因此线程切换的开销较小。

8、线程切换为什么比进程切换快,节省了什么资源?

        线程切换比进程切换快是因为线程共享同一进程的地址空间和资源,线程切换时只需切换堆栈和程序计数器等少量信息,而不需要切换地址空间,避免了进程切换时需要切换内存映射表等大量资源的开销,从而节省了时间和系统资源。

9、线程切换的详细过程是怎么样的?上下文保存在哪?

线程切换的详细过程可以分为以下几个步骤:
·上下文保存:当操作系统决定切换到另一个线程时,它首先会保存当前线程的上下文信息。上下文信息包括寄存器状态、程序计数器、堆栈指针等,用于保存线程的执行状态。

·切换到调度器:操作系统将执行权切换到调度器(Scheduler)。调度器负责选择下一个要执行的线程,并根据调度算法做出决策。
·上下文恢复:调度器选择了下一个要执行的线程后,它会从该线程保存的上下文信息中恢复线程的执行状态。
·切换到新线程:调度器将执行权切换到新线程,使其开始执行。
        上下文信息的保存通常由操作系统负责管理,具体保存在哪里取决于操作系统的实现方式。一般情况下,上下文信息会保存在线程的控制块(ThreadControlBlock,TCB)中。

        TCB是操作系统用于管理线程的数据结构,包含了线程的状态、寄存器的值、堆栈信息等。当发生线程切换时,操作系统会通过切换TCB来保存和恢复线程的上下文信息。

10、进程的五种状态,如何切换?

进程五种状态的变迁
再来详细说明一下进程的状态变迁:
        ·NULL->创建状态:一个新进程被创建时的第一个状态;
        ·创建状态->就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
        ·就绪态->运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给CPU正式运行该进程;
        ·运行状态->结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
        ·运行状态->就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
        ·运行状态->阻塞状态: 当进程请求某个事件且必须等待时, 例如请求 I/O事件;
        ·阻塞状态->就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;

11、进程上下文有哪些?

        各个进程之间是共享CPU资源的,在不同的时候进程之间需要切换,让不同的进程可以在CPU执行,那么 一个进程切换到另一个进程运行,称为进程的上下文切换。
        在详细说进程上下文切换前,我们先来看看CPU上下文切换

        大多数操作系统都是多任务,通常支持大于CPU数量的任务同时运行。实际上,这些任务并不是同时运行的,只是因为系统在很短的时间内,让各个任务分别在CPU运行,于是就造成同时运行的错觉。
        任务是交给CPU运行的,那么在每个任务运行前,CPU需要知道任务从哪里加载,又从哪里开始运行。
        所以,操作系统需要事先帮CPU设置好CPU寄存器和程序计数器。
        CPU寄存器是CPU内部一个容量小,但是速度极快的内存(缓存)。我举个例子,寄存器像是你的口袋,内存像你的书包,硬盘则是你家里的柜子,如果你的东西存放到口袋,那肯定是比你从书包或家里柜子取出来要快的多。
        再来,程序计数器则是用来存储CPU正在执行的指令位置、或者即将执行的下一条指令位置。
所以说,CPU寄存器和程序计数是CPU在运行任何任务前,所必须依赖的环境,这些环境就叫做CPU上下文。
        既然知道了什么是CPU上下文,那理解CPU上下文切换就不难了。
        CPU上下文切换就是先把前一个任务的CPU上下文(CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
        系统内核会存储保持下来的上下文信息,当此任务再次被分配给CPU运行时,CPU会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行.
        上面说到所谓的[任务],主要包含进程、线程和中断。所以,可以根据任务的不同,把CPU上下文切换分成:进程上下文切换、线程上下文切换 和 中断上下文切换

进程上下文的切换到底切换了什么?

        进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
        所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源
        通常,会把交换的信息保存在进程的PCB,当要运行另外一个进程的时候,我们需要从这个进程的PCB取出上下文,然后恢复到CPU中,这使得这个进程可以继续执行,如下图所示:

        大家需要注意,进程的上下文开销是很关键的,我们希望它的开销越小越好,这样可以使得进程可以把更多时间花费在执行程序上,而不是耗费在上下文切换。

12、Linux 内核提供了不少进程间通信的方式

        ·管道
        ·消息队列
        ·共享内存
        ·信号
        ·信号量
        · socket

Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道

管道分为 [匿名管道]和 [命名管道]
        匿名通道:匿名管道顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell命令中的[|]竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道。再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {int pipe_fd[2]; // 管道文件描述符数组pid_t pid;char buffer[100];// 创建管道if (pipe(pipe_fd) == -1) {perror("pipe");exit(EXIT_FAILURE);}pid = fork(); // 创建子进程if (pid < 0) {perror("fork");exit(EXIT_FAILURE);}if (pid == 0) {// 子进程:读取数据close(pipe_fd[1]); // 关闭写端read(pipe_fd[0], buffer, sizeof(buffer));printf("子进程收到数据:%s\n", buffer);close(pipe_fd[0]); // 关闭读端} else {// 父进程:写入数据close(pipe_fd[0]); // 关闭读端write(pipe_fd[1], "Hello from parent!", 20);close(pipe_fd[1]); // 关闭写端wait(NULL); // 等待子进程结束}return 0;
}

        命名通道:命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为p的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 Iseek 之类的文件定位操作。

// server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>#define FIFO_NAME "/tmp/my_fifo"int main() {int fd;char buffer[100];// 创建命名管道if (mkfifo(FIFO_NAME, 0666) == -1) {perror("mkfifo");exit(EXIT_FAILURE);}printf("等待客户端连接...\n");// 打开命名管道用于写入fd = open(FIFO_NAME, O_WRONLY);if (fd == -1) {perror("open");exit(EXIT_FAILURE);}write(fd, "Hello from server!", 18);close(fd);// 删除命名管道unlink(FIFO_NAME);return 0;
}// client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>#define FIFO_NAME "/tmp/my_fifo"int main() {int fd;char buffer[100];// 打开命名管道用于读取fd = open(FIFO_NAME, O_RDONLY);if (fd == -1) {perror("open");exit(EXIT_FAILURE);}read(fd, buffer, sizeof(buffer));printf("客户端收到数据:%s\n", buffer);close(fd);return 0;
}

关于命名通道的阻塞问题:

FIFO的open()调用遵循以下规则:

        以只读方式(O_RDONLY)打开:如果当前没有以写方式打开的进程,open()会阻塞,直到有写端打开。
        以只写方式(O_WRONLY)打开:如果当前没有以读方式打开的进程,open()会阻塞,直到有读端打开。
        以读写方式(O_RDWR)打开:不会阻塞,因为读写端都在同一个进程中。


        消息队列:消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的[消息链表],消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。
        消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <mqueue.h>
#include <unistd.h>#define QUEUE_NAME "/my_queue"
#define MAX_MSG_SIZE 256
#define MSG_BUFFER_SIZE (MAX_MSG_SIZE + 10)int main(int argc, char *argv[]) {mqd_t mq;struct mq_attr attr;char buffer[MSG_BUFFER_SIZE];unsigned int prio;// 设置消息队列属性attr.mq_flags = 0;attr.mq_maxmsg = 10;attr.mq_msgsize = MAX_MSG_SIZE;attr.mq_curmsgs = 0;if (argc > 1 && strcmp(argv[1], "send") == 0) {// 发送模式printf("发送模式\n");// 打开或创建消息队列mq = mq_open(QUEUE_NAME, O_CREAT | O_WRONLY, 0644, &attr);if (mq == (mqd_t)-1) {perror("mq_open");exit(EXIT_FAILURE);}// 发送消息char *msg = "Hello from sender!";if (mq_send(mq, msg, strlen(msg), 0) == -1) {perror("mq_send");exit(EXIT_FAILURE);}printf("消息发送成功: %s\n", msg);mq_close(mq);} else {// 接收模式printf("接收模式\n");// 打开消息队列mq = mq_open(QUEUE_NAME, O_RDONLY);if (mq == (mqd_t)-1) {perror("mq_open");exit(EXIT_FAILURE);}// 接收消息ssize_t bytes_read = mq_receive(mq, buffer, MSG_BUFFER_SIZE, &prio);if (bytes_read == -1) {perror("mq_receive");exit(EXIT_FAILURE);}buffer[bytes_read] = '\0';printf("接收到消息 (优先级 %u): %s\n", prio, buffer);mq_close(mq);mq_unlink(QUEUE_NAME); // 删除消息队列}return 0;
}

消息队列中相关的属性解释如下:

#include <mqueue.h>struct mq_attr {long mq_flags;    // 消息队列标志//可选值://    0:阻塞模式(默认)//    O_NONBLOCK:非阻塞模式long mq_maxmsg;   // 最大消息数量//作用:定义消息队列中最多能存储的消息条数//限制://1)必须大于 0//2)不能超过系统限制(通常在 /proc/sys/fs/mqueue/msg_max 中定义)//3)默认值通常是 10long mq_msgsize;  // 最大消息大小(字节)//1)必须大于 0//2)不能超过系统限制(通常在 /proc/sys/fs/mqueue/msgsize_max 中定义)//3)默认值通常是 8192 字节(8KB)long mq_curmsgs;  // 当前队列中的消息数//只读属性,表示当前队列中的消息数量//注意://1)创建时设置为 0//2)不能在创建时指定//3)通过 mq_getattr() 获取当前值
};

查看系统消息队列:

# 查看所有消息队列
ipcs -q# 查看特定消息队列详细信息
ipcs -q -i <msgid># 删除消息队列
ipcrm -q <msgid>


        共享内存:共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。

        那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是P操作和V操作。

        共享内存怎么实现的?
        共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。

//生产者#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <semaphore.h>#define SHM_NAME "/my_shm"
#define SEM_PRODUCER "/sem_producer"
#define SEM_CONSUMER "/sem_consumer"
#define BUFFER_SIZE 1024// 共享内存结构
struct shared_data {char buffer[BUFFER_SIZE];int data_ready;  // 标志位:1表示有数据,0表示无数据
};int main() {int shm_fd;struct shared_data *shared_mem;sem_t *sem_producer, *sem_consumer;printf("生产者进程启动...\n");// 1. 创建共享内存shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);if (shm_fd == -1) {perror("shm_open");exit(EXIT_FAILURE);}// 设置共享内存大小if (ftruncate(shm_fd, sizeof(struct shared_data)) == -1) {perror("ftruncate");exit(EXIT_FAILURE);}// 映射共享内存shared_mem = mmap(0, sizeof(struct shared_data), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);if (shared_mem == MAP_FAILED) {perror("mmap");exit(EXIT_FAILURE);}// 2. 创建信号量// 生产者信号量:初始值为1,表示生产者可以写入sem_producer = sem_open(SEM_PRODUCER, O_CREAT, 0666, 1);if (sem_producer == SEM_FAILED) {perror("sem_open producer");exit(EXIT_FAILURE);}// 消费者信号量:初始值为0,表示没有数据可读sem_consumer = sem_open(SEM_CONSUMER, O_CREAT, 0666, 0);if (sem_consumer == SEM_FAILED) {perror("sem_open consumer");exit(EXIT_FAILURE);}// 3. 生产数据并写入共享内存for (int i = 0; i < 5; i++) {char message[100];sprintf(message, "Hello from producer - Message %d", i + 1);// 等待生产者信号量(获取写入权限)printf("生产者等待写入权限...\n");if (sem_wait(sem_producer) == -1) {perror("sem_wait producer");exit(EXIT_FAILURE);}// 写入数据到共享内存strcpy(shared_mem->buffer, message);shared_mem->data_ready = 1;printf("生产者写入: %s\n", message);// 释放消费者信号量(通知消费者可以读取)if (sem_post(sem_consumer) == -1) {perror("sem_post consumer");exit(EXIT_FAILURE);}sleep(2);  // 模拟生产时间}// 发送结束信号if (sem_wait(sem_producer) == -1) {perror("sem_wait producer");exit(EXIT_FAILURE);}strcpy(shared_mem->buffer, "END");shared_mem->data_ready = 1;printf("生产者发送结束信号\n");if (sem_post(sem_consumer) == -1) {perror("sem_post consumer");exit(EXIT_FAILURE);}// 4. 清理资源sleep(2);  // 等待消费者处理完// 取消映射if (munmap(shared_mem, sizeof(struct shared_data)) == -1) {perror("munmap");exit(EXIT_FAILURE);}// 关闭共享内存if (close(shm_fd) == -1) {perror("close");exit(EXIT_FAILURE);}// 关闭信号量sem_close(sem_producer);sem_close(sem_consumer);printf("生产者进程结束\n");return 0;
}// 消费者#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <semaphore.h>#define SHM_NAME "/my_shm"
#define SEM_PRODUCER "/sem_producer"
#define SEM_CONSUMER "/sem_consumer"
#define BUFFER_SIZE 1024// 共享内存结构
struct shared_data {char buffer[BUFFER_SIZE];int data_ready;
};int main() {int shm_fd;struct shared_data *shared_mem;sem_t *sem_producer, *sem_consumer;printf("消费者进程启动...\n");// 1. 打开共享内存shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);if (shm_fd == -1) {perror("shm_open");exit(EXIT_FAILURE);}// 映射共享内存shared_mem = mmap(0, sizeof(struct shared_data), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);if (shared_mem == MAP_FAILED) {perror("mmap");exit(EXIT_FAILURE);}// 2. 打开信号量sem_producer = sem_open(SEM_PRODUCER, 0);if (sem_producer == SEM_FAILED) {perror("sem_open producer");exit(EXIT_FAILURE);}sem_consumer = sem_open(SEM_CONSUMER, 0);if (sem_consumer == SEM_FAILED) {perror("sem_open consumer");exit(EXIT_FAILURE);}// 3. 读取数据while (1) {// 等待消费者信号量(等待数据)printf("消费者等待数据...\n");if (sem_wait(sem_consumer) == -1) {perror("sem_wait consumer");exit(EXIT_FAILURE);}// 检查是否是结束信号if (strcmp(shared_mem->buffer, "END") == 0) {printf("消费者收到结束信号,退出...\n");// 释放生产者信号量if (sem_post(sem_producer) == -1) {perror("sem_post producer");exit(EXIT_FAILURE);}break;}// 读取数据printf("消费者读取: %s\n", shared_mem->buffer);// 释放生产者信号量(通知生产者可以写入)if (sem_post(sem_producer) == -1) {perror("sem_post producer");exit(EXIT_FAILURE);}sleep(1);  // 模拟处理时间}// 4. 清理资源// 取消映射if (munmap(shared_mem, sizeof(struct shared_data)) == -1) {perror("munmap");exit(EXIT_FAILURE);}// 关闭共享内存if (close(shm_fd) == -1) {perror("close");exit(EXIT_FAILURE);}// 关闭信号量sem_close(sem_producer);sem_close(sem_consumer);// 删除信号量和共享内存(只需要一个进程删除)sem_unlink(SEM_PRODUCER);sem_unlink(SEM_CONSUMER);shm_unlink(SHM_NAME);printf("消费者进程结束\n");return 0;
}


        与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源 (如键盘 Cltr+C)和软件来源 (如 kill 命令),一旦有信号发生,进程有三种方式响应信号1.执行默认操作、2.捕捉信号、3.忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。

        socket网络通信:前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。

13、线程间通讯有什么方式?

Linux系统提供了五种用于线程通信的方式:  互斥锁、 读写锁、 条件变量、 自旋锁和信号量。

·互斥锁(Mutex):互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥锁加锁的线程将会阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥锁加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。


作用:提供基本的线程同步,防止多个线程同时访问共享资源
原理:锁定临界区,只允许一个线程进入

#include <stdio.h>
#include <pthread.h>int counter = 0;
pthread_mutex_t lock; // 互斥锁声明void* increment(void* arg) {for (int i = 0; i < 10000; i++) {pthread_mutex_lock(&lock); // 加锁counter++; // 临界区操作pthread_mutex_unlock(&lock); // 解锁}return NULL;
}int main() {pthread_t t1, t2;// 初始化互斥锁pthread_mutex_init(&lock, NULL);// 创建两个线程pthread_create(&t1, NULL, increment, NULL);pthread_create(&t2, NULL, increment, NULL);// 等待线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);printf("最终计数器值: %d\n", counter); // 应为20000pthread_mutex_destroy(&lock);return 0;
}

·读写锁(Read-Write Locks):读写锁从字面意思我们也可以知道,它由[读锁]和[写锁]两部分构成,如果只读取共享资源用[读锁]加锁,如果要修改共享资源则用[写锁]加锁。所以,读写锁适用于能明确区分读操作和写操作的场景。读写锁的工作原理是:当[写锁]没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为[读锁]是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。但是,一旦[写锁]被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。知道了读写锁的工作原理后,我们可以发现,读写锁在读多写少的场景,能发挥出优势。

作用:允许多个读操作并行,但写操作独占
原理:读操作可共享,写操作需互斥

#include <stdio.h>
#include <pthread.h>int data = 0;
int readers = 0;
pthread_rwlock_t rwlock; // 读写锁声明void* reader(void* arg) {for (int i = 0; i < 5; i++) {pthread_rwlock_rdlock(&rwlock); // 获取读锁printf("Reader %ld: 数据 = %d (当前读者数: %d)\n", (long)arg, data, ++readers);readers--;pthread_rwlock_unlock(&rwlock); // 释放读锁sleep(1);}return NULL;
}void* writer(void* arg) {for (int i = 0; i < 3; i++) {pthread_rwlock_wrlock(&rwlock); // 获取写锁data++;printf("Writer: 更新数据为 %d\n", data);pthread_rwlock_unlock(&rwlock); // 释放写锁sleep(2);}return NULL;
}int main() {pthread_t r1, r2, w;pthread_rwlock_init(&rwlock, NULL);pthread_create(&r1, NULL, reader, (void*)1);pthread_create(&r2, NULL, reader, (void*)2);pthread_create(&w, NULL, writer, NULL);pthread_join(r1, NULL);pthread_join(r2, NULL);pthread_join(w, NULL);pthread_rwlock_destroy(&rwlock);return 0;
}

·条件变量 (Condition Variables):条件变量(cond)是在多线程程序中用来实现"等待--》唤醒"逻辑常用的方法。条件变量利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。线程在改变条件状态前必须首先锁住互斥量,函数pthread cond wait把自己放到等待条件的线程列表上,然后对互斥锁解锁(这两个操作是原子操作)。在函数返回时,互斥量再次被锁住。

作用:线程间通知机制,用于等待特定条件成立
原理:结合互斥锁,实现线程等待/通知机制

#include <stdio.h>
#include <pthread.h>int task_ready = 0; // 任务就绪标志
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 条件变量void* worker(void* arg) {pthread_mutex_lock(&mutex);// 等待任务就绪while (!task_ready) {printf("Worker: 等待任务...\n");pthread_cond_wait(&cond, &mutex); // 等待条件}printf("Worker: 开始处理任务\n");pthread_mutex_unlock(&mutex);return NULL;
}void* manager(void* arg) {sleep(2); // 模拟准备时间pthread_mutex_lock(&mutex);task_ready = 1; // 设置任务就绪printf("Manager: 任务已分配\n");pthread_cond_signal(&cond); // 通知等待的线程pthread_mutex_unlock(&mutex);return NULL;
}int main() {pthread_t t_worker, t_manager;pthread_create(&t_worker, NULL, worker, NULL);pthread_create(&t_manager, NULL, manager, NULL);pthread_join(t_worker, NULL);pthread_join(t_manager, NULL);return 0;
}


· 自旋锁 (Spinlock): 自旋锁通过 CPU 提供的 CAS 函数 (Compare And Swap), 在 [用户态]完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。一般加锁的过程,包含两个步骤:第一步,查看锁的状态,如果锁是空闲的,则执行第二步;第二步,将锁设置为当前线程持有;使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会[忙等待],直到它拿到锁。CAS函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。这里的[忙等待]可以用while循环等待实现,不过最好是使用CPU提供的PAUSE指令来实现[忙等待],因为可以减少循环等待时的耗电量。

作用:忙等待锁,适用于锁持有时间短的场景
原理:线程在等待时不休眠,而是循环检查锁状态

#include <stdio.h>
#include <pthread.h>int counter = 0;
pthread_spinlock_t spinlock; // 自旋锁声明void* increment(void* arg) {for (int i = 0; i < 10000; i++) {pthread_spin_lock(&spinlock); // 获取自旋锁counter++;pthread_spin_unlock(&spinlock); // 释放自旋锁}return NULL;
}int main() {pthread_t t1, t2;pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);pthread_create(&t1, NULL, increment, NULL);pthread_create(&t2, NULL, increment, NULL);pthread_join(t1, NULL);pthread_join(t2, NULL);printf("最终计数器值: %d\n", counter); // 应为20000pthread_spin_destroy(&spinlock);return 0;
}

·信号量(Semaphores):信号量可以是命名的(有名信号量)或无名的(仅限于当前进程内的线程),用于控制对资源的访问次数。通常信号量表示资源的数量,对应的变量是一个整型(sem)变量。另外,还有两个原子操作的系统调用函数来控制信号量的,分别是:P操作:将sem减1,相减后,如果sem<0,则进程/线程进入阻塞等待,否则继续,表明P操作可能会阻塞;V操作:将sem加1,相加后,如果sem<=0,唤醒一个等待中的进程/线程,表明V操作不会阻塞;

作用:控制对共享资源的访问数量
原理:维护一个计数器,限制同时访问的线程数

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>#define THREADS 5
#define MAX_CONCURRENT 2sem_t sem; // 信号量声明void* task(void* arg) {int id = *(int*)arg;sem_wait(&sem); // P操作(等待信号量)printf("线程 %d: 开始访问资源\n", id);sleep(2); // 模拟资源使用printf("线程 %d: 释放资源\n", id);sem_post(&sem); // V操作(释放信号量)return NULL;
}int main() {pthread_t threads[THREADS];int ids[THREADS];// 初始化信号量(允许MAX_CONCURRENT个线程同时访问)sem_init(&sem, 0, MAX_CONCURRENT);for (int i = 0; i < THREADS; i++) {ids[i] = i + 1;pthread_create(&threads[i], NULL, task, &ids[i]);}for (int i = 0; i < THREADS; i++) {pthread_join(threads[i], NULL);}sem_destroy(&sem);return 0;
}

14、除了互斥锁你还知道什么锁?分别应用于什么场景?

        还有读写锁、自旋锁、条件变量、信号量。
        1.读写锁:读写锁允许多个线程同时读取共享资源,但只允许一个线程进行写操作。适用于读操作频繁、写操作较少的场景,可以提高并发性能。
        2.自旋锁:自旋锁是一种忙等待锁,线程在获取锁时不会进入阻塞状态,而是循环忙等待直到获取到锁。适用于临界区很小且锁的持有时间很短的场景,避免线程频繁切换带来的开销。
        3.条件变量:条件变量用于线程间的同步和通信。它通常与互斥锁一起使用,线程可以通过条件变量等待某个条件满足,当条件满足时,其他线程可以通过条件变量发送信号通知等待线程。
        4.信号量:信号量是一种计数器,用于控制对共享资源的访问。它可以用来限制同时访问资源的线程数量,或者用于线程间的同步。


 

http://www.dtcms.com/a/509057.html

相关文章:

  • 中山微网站建设报价重庆网站建设 重庆网站制作
  • 做视频网站需要什么软件有哪些云电脑免费版永久使用
  • 常州城乡建设局网站网站最新程序策划书
  • 环保局网站建设申请网络游戏名字
  • 阿里Qoder 【新手一小时0-1开发前后端系统】附详细过程
  • 网络营销型网站策划中国软件是外包公司吗
  • 网站建设与管理怎么做塘沽网站优化
  • DeepSeek-AI团队开源的DeepSeek-OCR模型
  • 上海建筑网站建设wordpress添加活动
  • 笔试-模拟打印
  • 中堂东莞网站建设网站界面设计中的版式设计有哪些
  • missionplanner飞行模式参数调参释义一览
  • 撰写网站建设策划书范文专业制作结婚证
  • Pangolin安装记录
  • 网站seo排名优化价格附近的广告公司电话
  • 手机上的应用商店seo做多个网站
  • wordpress表单统计插件西安seo排名扣费
  • 二级网站有什么好处网站怎么做移动适配
  • 中国建设银行网站类型分析特别好的企业网站程序
  • 二分搜索及一些应用
  • 高效编程——电脑配置与效率提升完全指南
  • 东莞网站建设关键词链接提交工具的推荐词
  • 网站建设项目心得体会海拉尔网站制作
  • 基于 seajs 的高性能网站开发和优化实践_王保平(淘宝)中山外贸网站建设价格
  • 做网站教程视频wordpress 301错误
  • 深圳网站优化服务重庆网站制作机构
  • 网站 手机版网站开发合同东莞网站搭建建站公司
  • 如何做网站域名解析石家庄最新封闭小区消息
  • OpenHarmony 之face_auth人脸驱动源码级拆解:v1.0→v2.0 架构演化
  • 顺义制作网站房屋设计装修软件免费