用户态和内核态

用户态和内核态的区别?

  • 内核态(Kernel Mode):在内核态下,CPU可以执行所有的指令和访问所有的硬件资源。执行特权指令。

  • 用户态(User Mode):在用户态下,CPU只能执行部分指令集,无法直接访问硬件资源。执行用户指令。

指令集:MOV EAX, [0x1000]。汇编后的或者编译后的代码

进程管理

线程和进程的区别是什么?

本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
源码中的全局变量属于进程,这个全局变量就可以是 进程的硬件呀
线程是执行的基本单位。因为底下这句代码

pthread_create(&thread1, NULL, task, (void*)"数据加载");
//task就是这个线程执行的任务,cpu就分给这个任务

进程,线程,协程的区别是什么?

协程不会,不学了

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

虚拟内存:每个进程都会有一个虚拟内存,虚拟地址再 = 》 物理地址。父子进程都会即使有相同 的虚拟内存,但虚拟地址=》物理地址不同
文件描述符: 父子进程都会有相同的fd,但是是两个完全一样的,并不是一个

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

进程切换 换的是内存
线程切换 换的是 线程带着的寄存器(寄存器是小内存嘛)

进程上下文有哪些?

首先明白 cpu上下文 CPU 寄存器(cache)和程序计数器(PC)
根据cpu寄存器中存储的【任务】,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换。

所以 进程的上下文切换就是把 进程所管的用户内存空间的内存换了一下,内核空间换了一下。而切换的东西都记在PCB中

进程间通讯有哪些方式?

  1. 在我们的用户内存空间之外还有 内核空间
  2. 内核空间中有 进程间通信的 帮手
匿名管道
  1. 父子进程间拿 一个fd 交流
  2. 内核空间提供一段内核空间内存当【缓冲区】
    【缓冲区】是为了 缓冲父子进程间时机不匹配 一个想do一个不想do时 就放在【缓冲区】
  int pfd[2]; // pfd[0] 读端, pfd[1] 写端
  char buf[1024];
  read(pfd[0], buf, sizeof(buf));
命名管道
  1. 两进程 约定一个有名字的管道(管道也是文件类型的一种)交流一下
  2. 内核空间提供一段内核空间内存当【管道】
 	const char *fifo_name = "/tmp/my_fifo";
   
    //  创建命名管道(如果已存在,mkfifo会失败,所以这里忽略错误)
    mkfifo(fifo_name, 0666); // 权限 0666
消息队列
  1. 有一个meg的结构体,有了原子性,不再和管道一样是二进制的流
  2. 会先将消息写在用户内存空间中,再 复制到 内核空间中的消息队列上
strcpy(msg_send.mtext, "Hello Message Queue!");
msgsnd(msqid, &msg_send, sizeof(msg_send.mtext), 0); // 注意长度计算
共享内存
  1. 开辟的共享内存就在 用户内存空间中(不在内核空间中)
  2. 进程A在虚拟内存中开辟一块空间,进程A在虚拟内存中开辟一块空间,通过mmap,映射到同一块物理内存中。
  3. 所以就有了 进程间的 同步问题

在这里插入图片描述

信号

在命令行使用 kill -9 1234命令,就是 shell 进程通过 kill系统调用,向 PID 为 1234 的进程发送了一个 SIGKILL信号,要求它强制终止。

线程间通讯有什么方式?

原子操作

原子操作:只能保护一个变量
比如 std::atomic count; count++;。它只能保证这一个数字的加减是绝对安全的,不能被拆分的。

锁机制:保护的是一段复杂的代码块(临界区)

原子操作(纯硬件级):依赖 CPU 的特定指令
机制(OS 软件级):也有一部分原子操作,但是也依赖操作系统的调度。

原子操作:遇到另一个原子操作时。是“忙等”的,线程一直是运行态。
锁机制:会在os的调度下,从运行态=》阻塞态。

互斥锁:

如果进线遇到互斥,主动放弃CPU,从运行态=》阻塞态。(与自旋锁的区别)
当我上锁时,其他人都不能来读。(与读写锁的区别)

在这里插入图片描述

自旋锁

通过TAS或者CAS指令实现的。
如果进线遇到互斥,线程会一直循环尝试获取锁,一直处于运行态。

在这里插入图片描述

条件变量

互斥锁是线程间互斥的机制,条件变量则是同步机制。

写出条件变量得知道这几个点。

  1. unique_lock lock(mtx);只有这个智能锁有mtx的所有权
    在这个【有参构造函数】中,会自动给对象lock上锁。
    cv.wait(lock)时,又会自动给对象lock解锁。
  2. 阻塞时,必须是这个模板。
while (条件不满足) {
    cv.wait(lock);
}
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

using namespace std;

mutex mtx;
// 💡 听你的!搞两个条件变量,泾渭分明!
condition_variable cv_odd;  
condition_variable cv_even; 

int count_num = 1;
const int MAX_NUM = 100;

// ==========================================
// 奇数线程
// ==========================================
void printOdd() {
    while (count_num <= MAX_NUM) {
        unique_lock<mutex> lock(mtx);
        
        // 1. 我在奇数专属的床上睡觉
        while (count_num % 2 == 0 && count_num <= MAX_NUM) {
            cv_odd.wait(lock); 
        }
        
        if (count_num > MAX_NUM) break;
        
        cout << "奇数线程: " << count_num << endl;
        count_num++; 
        
        // 2. 💡 核心绝杀:我不随便叫人了!我精准地按响偶数线程床头的闹钟!
        cv_even.notify_one(); 
    }
}

// ==========================================
// 偶数线程
// ==========================================
void printEven() {
    while (count_num <= MAX_NUM) {
        unique_lock<mutex> lock(mtx);
        
        // 1. 我在偶数专属的床上睡觉
        while (count_num % 2 != 0 && count_num <= MAX_NUM) {
            cv_even.wait(lock); 
        }     
        
        cout << "偶数线程: " << count_num << endl;
        count_num++; 
        
        // 2. 💡 核心绝杀:精准按响奇数线程床头的闹钟!
        cv_odd.notify_one(); 
    }
}

int main() {
    thread t1(printOdd);
    thread t2(printEven);

    t1.join();
    t2.join();

    cout << "打印完毕!" << endl;
    return 0;
}
利用信号量实现的生产者-消费者

使用2个信号量,一个生产,一个消费

利用信号量实现的读写锁

使用2个信号量,一个读写
一个互斥

在这里插入图片描述

读写锁

1个锁。用unique_lock /和std::shared_lock来实现
多个读者可以同时进行读
写者必须互斥(只允许一个写者写,也不能读者写者同时进行)

有一个独享所有权的写锁,和一把共享所有权的读锁

写锁
在这里插入图片描述
读锁
在这里插入图片描述

无锁队列

一个队列,如果最后这个结点的下一个是null,我就插入新的结点。如果有人插队,我就后移一下尾结点,再CAS。

自旋锁是什么?应用在哪些场景?

自旋锁就是 CPU 提供的 CAS 函数(Compare And Swap)
当锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁

互斥锁VS自旋锁

  • 互斥锁 当cpu上有上锁的进程a时,进程b调用了PCB恢复了现场,试了一下,发现不行,就又回到阻塞态了。

  • 在这里插入图片描述

  • 自旋锁 当cpu上有上锁的进程a时,进程b调用了PCB恢复了现场,试了一下,发现不行,还会一直在Run态一直试。好处是:不用频繁调用PCB恢复现场。

乐观锁和悲观锁有什么区别?

  • 悲观锁 以防有人害我
    像MySQL中的,不管有没有人用,我先用【记录锁】锁住。
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 🔒 锁定数据
  • 乐观锁
    就像自旋锁一样。即使忙等,我也只用等一会会。

介绍一下brk,mmap

mmap()函数是内存映射函数。在文件映射区域偷一块内存。

网络 i/o

你了解过哪些io模型?

  • 你了解io吗?
    io过程是指 数据在内存(进程的用户空间 buffer)与外部设备之间(如socket buffer内核空间,硬盘)的传输过程
    read(),write()函数是io函数

  • 阻塞
    客户端进程因为某些事故 卡在了代码的一处

  • 阻塞IO模型:


int main() {

    
    // 1. 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("Socket creation error");
        exit(EXIT_FAILURE);
    }  
  
    
    // 2. 连接服务器 
    printf("Connecting to server...\n");
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Connection Failed");
        exit(EXIT_FAILURE);
    }
 
    
    // 3. 发送数据
    printf("Sending message: %s\n", hello);
    send(sock, hello, strlen(hello), 0);
    
    //阻塞IO
    // 这个 read() 调用会阻塞客户端进程
    int valread = read(sock, buffer, BUFFER_SIZE);
    
   
    // 4. 关闭套接字
    close(sock);
    printf("Client closed\n");
    
    return 0;
}
  • 非阻塞IO模型
    1. 非阻塞:将一个fd(一块内核空间) 设置为非阻塞状态。进程在io时 如果read不到数据到buffer中,就会(1)稍后轮询 (2)IO复用

    2. IO复用:使用了epoll等机制,将该fd注册到epoll上。epoll此时像一个小秘书,一旦fd发生变化(fd满了可读/fd空了可写),epoll就会通知进程快来IO。这个小秘书还管上百个fd,这就是复用。

select、poll、epoll 的区别是什么?

select:将需要监视的fd放到文件描述符集合中,数据结构是位图。一次次遍历这个位图,标记为是否可读。需要处理读写时需要再遍历第二遍。
poll:--------------------------------------------------------------------用户空间中是用数组,内核中是用链表

事件驱动机制:事件驱动(一有事,就触发某事) vs select的轮询机制(一直问我能不能do)。事件驱动,通过回调函数实现。比如epoll中,epoll监视epfd,一旦sockfd的内存中有变化,就会给到events数组中。一有事件,就放到events数组中。

epoll
在这里插入图片描述

  1. epoll_create( )创建树
  2. epoll_ctl( ):将sockfd添加到树上,在树上的都会被监视
  3. 触发:(1)LT模式,sockfd的内存中有数据,就会一直往evets数组中给,epoll_wait( )几秒钟就会读一次,就读到他。(2)ET模式。sockfd的内存中只有状态变化时,才会往events数组中给
  4. epoll_wait( ):一段时间就会统计events数组中的个数。

代码加强理解触发模式
LT。假设客户端发送了 1000 字节数据,服务器每次读取 500 字节。

// 第一次发现一个sockfd中有数据。立马触发。epoll_wait返回1
n = epoll_wait(epoll_fd, events, 10, 1000); // 返回1个事件
read(sockfd, buf, 500); // 服务端读取500字节

// 第二次发现sockfd中还有数据。立马触发。epoll_wait返回1
n = epoll_wait(epoll_fd, events, 10, 1000); // 再次返回1个事件(还有500字节可读)
read(sockfd, buf, 500); // 服务端读取剩余500字节

// 第三次sockfd中没有数据。epoll_wait返回0
n = epoll_wait(epoll_fd, events, 10, 1000); // 无事件(阻塞或超时)

redis,nginx,netty 是依赖什么做的这么高性能?

Reactor模式:1.对于高性能服务器的一种开发模型。
2.[反应堆]=====》【对事件的反应堆】= ===》【一有事件,Reactor就会有反应】

惊群现象

当多个进程/线程在等待同一个资源(如网络连接、锁、信号量)时,当资源可用(事件发生),系统会唤醒所有等待者,但最终只有一个进程/线程能获得该资源,其他进程/线程白忙一场后重新进入等待状态。.
.

惊群现象发生的典型场景

  1. 网络服务器中的 accept() 惊群(经典案例)
    当一个新连接到达时,​所有4个子进程都会从 accept() 阻塞中唤醒【阻塞态】,但只有一个进程能成功接受这个连接,其他3个进程"扑了个空"【一瞬间的就绪态】,需要重新进入【阻塞状】.(都上了一遍cpu,运转了一会发现不合适又下来了------cpu上下文切换)
  2. 解决方案: 使用互斥锁保护 accept()(传统方案)
  3. 用上了锁不还是会有惊群现象吗?
    是的还是会有,从大惊群变成了小惊群。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐