操作系统:进程篇

进程与线程
1、在内存中运行的程序就是进程,线程是进程的一条流程。由于进程是资源分配的基本单位,当进程阻塞,它的所有资源都不能使用。为了提高资源的利用率和并发度,引入了线程。一个进程就是由多个线程组成的,线程创建销毁开销很小。
2、同一进程的线程共享同一个进程的虚拟地址空间如堆、全局区,所以一个进程内的线程通信也很方便(若一个线程new了,其他线程也是能访问这块堆区内存的)。但每个线程拥有自己的栈(因为一个线程就是一个回调函数)
协程: 可以认为它是轻量级线程,协程是可以暂停和恢复的函数。普通函数只能从头运行到尾,而协程的话可以暂停,让出CPU,然后调度其他协程运行。如一个协程处理网络IO,阻塞了,这时候就可以让出来执行其他协程,等阻塞结束再恢复。好处:提高并发,且协程切换也更轻量级。
多线程与多进程如何选择
对于cpu密集型任务,选多进程充分利用多核cpu的并行能力,若采用多线程,需要竞争cpu,造成性能瓶颈。
对于io密集型任务,经常需要等待外部资源,cpu大部分时间是空闲的,一般采用多线程,如在a线程等待io时让出cpu执行其他任务,提高cpu利用率。

进程状态:
在这里插入图片描述
刚创建出来是创建态,初始化完成可以运行了变为就绪态进入就绪队列,当被调度程序选中上cpu运行,进入运行态,当时间片用完回到就绪列/运行结束即结束态/阻塞被调度下cpu进入阻塞队列即阻塞态,被唤醒回到就绪队列。

进程间通信方式:进程都是独立的,想相互通信要通过内核。5种
管道:ls|grep mysql,这个|就是匿名管道,只能用于有亲缘关系的进程通信,因为…。有名管道也叫FIFO可以任意进程。管道缺点:单向,且缓冲区有限。
消息队列:流程:电子邮件,a进程发消息给b,把数据放到对应的消息队列就返回了,b需要时再读即可。消息队列本质是内核的消息链表。缺点:对消息队列的读写存在拷贝的开销。
共享内存:解决消息队列数据拷贝的问题:2个进程的虚拟地址映射同一块物理内存,直接通过物理内存交流,就不需要拷贝了。但要注意数据安全(pthread_mutex)。
信号:进程收到信号后会处理(默认动作、自定义动作即信号捕捉、忽略)
Socket:实现不同主机的进程间通信。
常用的就是socket(网络通信)和共享内存(同一个系统上)。
共享内存有System V 共享内存(shmget)、POSIX 共享内存(shm_open,较新),我们这里就用POSIX 共享内存示例:

//config.h
#progma once

//定义共享内存名、共享信号量名;同步也可用互斥锁,但要额外设置(std::mutex是不能用于进程间同步的)
#define SHM_NAME "/posix_shm"
#define SEM_NAME "/posix_sem"
#define SHM_SIZE 1024
// write.cpp
#include "config.h"
#include <iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <cstring>
#include <unistd.h>
#include <signal.h>

sem_t *sem = nullptr;
int shm_fd = -1;
char *shared_memory = nullptr;

void cleanup() {
    if (shared_memory) {
        munmap(shared_memory, SHM_SIZE);
    }
    if (shm_fd != -1) {
        shm_unlink(SHM_NAME);
    }
    if (sem) {
        sem_close(sem);
        sem_unlink(SEM_NAME);
    }
}

void signal_handler(int signum) {
    cleanup();
    exit(0);
}

int main() {
	//防止意外退出,资源释放不了:/dev/shm/,但系统重启就会释放
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);

    // 创建共享内存
    shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (shm_fd == -1) {
        perror("shm_open failed");
        return 1;
    }
    ftruncate(shm_fd, SHM_SIZE);

    // 映射共享内存
    shared_memory = static_cast<char *>(mmap(nullptr, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0));
    if (shared_memory == MAP_FAILED) {
        perror("mmap failed");
        return 1;
    }

    // 创建信号量
    sem = sem_open(SEM_NAME, O_CREAT, 0666, 1);
    if (sem == SEM_FAILED) {
        perror("sem_open failed");
        return 1;
    }

    // 写入数据到共享内存
    while (true) {
        sem_wait(sem);
        std::cout << "Enter a message: ";
        std::cin.getline(shared_memory, SHM_SIZE);
        sem_post(sem);
        sleep(1);
        if (strcmp(shared_memory, "exit") == 0) {
            break;
        }
    }

    cleanup();
    return 0;
}
// read.cpp
#include "config.h"
#include <iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <cstring>
#include <unistd.h>
#include <signal.h>

sem_t *sem = nullptr;
int shm_fd = -1;
char *shared_memory = nullptr;

void cleanup() {
    if (shared_memory) {
        munmap(shared_memory, SHM_SIZE);
    }
    if (shm_fd != -1) {
        close(shm_fd);
    }
    if (sem) {
        sem_close(sem);
    }
}

void signal_handler(int signum) {
    cleanup();
    exit(0);
}

int main() {
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);

    // 打开共享内存
    shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);
    if (shm_fd == -1) {
        perror("shm_open failed");
        return 1;
    }

    // 映射共享内存
    shared_memory = static_cast<char *>(mmap(nullptr, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0));
    if (shared_memory == MAP_FAILED) {
        perror("mmap failed");
        return 1;
    }

    // 打开信号量
    sem = sem_open(SEM_NAME, 0);
    if (sem == SEM_FAILED) {
        perror("sem_open failed");
        return 1;
    }

    // 读取数据
    while (true) {
        sem_wait(sem);
        if (strlen(shared_memory) > 0) {
            std::cout << "Received: " << shared_memory << std::endl;
            if (strcmp(shared_memory, "exit") == 0) {
                sem_post(sem);
                break;
            }
            memset(shared_memory, 0, SHM_SIZE);
        }
        sem_post(sem);
        sleep(1);
    }

    cleanup();
    return 0;
}

进程调度算法:也叫cpu调度算法:选择一个就绪进程上cpu。6大算法:
先来先服务:最简单,但对短作业不利。
最短作业优先:按运行时间长短排列运行,对长作业不利。
高响应比优先:按响应比排列=(等待时间+作业时间)/作业时间,大的先运行,权衡长短作业,相同作业时间等的长的先运行,相同等待时间作业时间短的先运行。
时间片轮转:设置一个时间片,每个进程运行一个时间片后下来。所有进程平等,不分优先级,可能是个缺点。
最高优先级:给进程设置优先级,抢占式:当就绪队列出现优先级更高的,直接强上cpu,非抢占:当前进程运行完再上。缺点:低优先级的可能永远不会执行。
多级反馈队列:多个队列,优先级从高到低,时间从低到高。进程按优先级放入相应的队列。永远运行高优先级队列里的进程,若时间片到了进程还没运行完,就移入下一级队列,下面的队列只有当上面的队列无进程才能被运行。他结合了前面的几种算法,按时间片优先运行高优先级进程,短作业可能很快就运行完了,长作业虽然要移入下面的队列,但运行时间也变长。
在这里插入图片描述
互斥:同一时间只能有一个进程或进程访问共享资源。常用互斥锁、信号量实现。
锁的底层原理:xxx
线程同步:多线程进行i++,步骤是这样的,先把i从内存拷贝到寄存器,在寄存器中++,然后拷贝回去。所以在a线程拷贝回去之前b线程也对其进行++,就是数据错误了。即多线程访问共享资源会导致数据安全问题,所以需要一些机制来保证线程对共享资源的安全访问。
1、互斥锁:只有拿到锁的线程才能访问;
2、读写锁shared_lock:允许多线程同时读,单线程写。适合读多写少场景
3、条件变量:配合锁使用,调用cond_wait释放锁阻塞线程,收到通知唤醒线程获得锁;
4、信号量:分计数信号量和二进制信号量,二进制信号量类似于互斥锁,计数信号量标记了资源的数量,允许多线程同时访问。

典型的生产者消费者模型:用一个互斥锁,2个信号量(1个是生产者有多少空位可以生产,一个是消费者有多少资源可以消费)。
2线程交替打印1-1000:

#include <iostream>
#include <thread>
#include <atomic>

int number = 1;
std::atomic<bool> flag(true); // 控制线程交替打印

void f1() {
    while (number <= 1000) {
        if (flag) {
            std::cout << "Thread 1: " << number++ << std::endl;
            flag = false;
        }
    }
}

void f2() {
    while (number <= 1000) {
        if (!flag) {
            std::cout << "Thread 2: " << number++ << std::endl;
            flag = true;
        }
    }
}

int main() {
    std::thread t1(f1);
    std::thread t2(f2);
    t1.join();
    t2.join();
}

哲学家就餐问题:也是经典同步。5个人一桌,每2人之间放一个叉子,只有拿到左右2个叉子才能就餐。如何实现最大化就餐?
法1:信号量:p拿筷子直到拿到2个,但可能死锁。
法2:加入互斥锁,当有人准备拿筷子,其他人不准动,但4个人看1个人吃效率低。法3:不能用互斥锁,让偶数编号的人先拿左筷子后拿右筷子,奇数相反,就不会死锁,也可2人进餐。
读者写者问题:有写时,不准读和其他人写;可以多人读。数据库访问大概就是这样。这个问题的实现要分几种情况:读者优先,当读者进入临界区,阻塞写者,当最后一个读者离开,唤醒写着。写者优先:当有写者进入,就阻塞后续读者,前面的读者读完就开始写。公平机制:就按队列顺序执行,都不互相阻塞。(shared_lock)
死锁:2个线程分别持有一份互斥的资源,且不可被抢占,现在又相互等待对方资源。
解决:破环相互等待获取1锁再获取2锁;打破抢占:强行抢资源;设置锁定超时时间;设定必须一次性获取所有资源,不然就失败。

锁的种类:最基本的互斥锁和自旋锁,都是独占锁。互斥锁:加锁失败,线程下cpu,阻塞,等锁被释放再唤醒,它有线程上下文切换的消耗,自旋锁会一直等待直到获得锁,会一直占用cpu,所以若你知道很快就能获得锁,就用自旋锁。读写锁:由互斥锁或自旋锁实现:写锁独占,读锁共享,所以在读多写少的情况下用它会比较好,也可以设置读优先锁,写优先锁,公平读写锁(获取锁的线程全排队)。前面3个锁都是悲观锁,他们认为并发访问共享资源很容易冲突,所以访问先要上锁。乐观锁:他认为冲突的概率很低,先修改共享资源,再验证这段时间内有没有冲突,若有放弃本次操作。如在线文档你总不能同时只能一个人编辑吧。
总结:互斥锁和自旋锁看着选,若要区分读写就用读写锁根据需要设置优先;者3个是悲观锁,若冲突概率低就要乐观锁。
IO阻塞、非阻塞、同步、异步
这里的阻塞非阻塞,同步异步都是针对网络IO来说的。
一次网络IO分2个阶段:数据准备(阻塞、非阻塞)和数据读写(同步、异步)
如recv函数,若是阻塞读取,没有数据:线程会进入阻塞状态;若是非阻塞,线程不会进入阻塞状态,通过返回值来判断(-1,0,>0,记得注意EAGAIN,EINTER等)。
当数据准备好了后(即内核缓冲区有数据了),来到第2阶段:数据读写(把内核缓冲区的数据搬到用户区的buf里,如read函数):谁搬的?若是应用程序搬的:这就是IO同步,消耗应用程序的时间;若是内核帮我们搬的,这就是异步IO,消耗内核的时间,内核搬好了通知应用程序(信号/回调),期间应用程序做自己的事。
只有使用了特殊的api才是异步io,其他的都是同步io。
注意:业务上的逻辑处理是同步还是异步(是否一定有序进行a->b),要区分开。

5种IO模型:同步阻塞IO(BIO)、同步非阻塞IO(NIO,一般是轮询检查)、异步IO、信号驱动IO、IO复用。

I/O多路复用: 前面学习了多进程多线程处理并发连接,一个连接对应一个进程或线程,对于高并发使用多进程多线程维护那么多进程或线程不现实。所以引入I/O多路复用:一个线程同时监听多个I/O也就是监听多个文件描述符,委托内核去检测哪些文件描述符发生变化(内核检测很快、位数组),进而把事件分发给子线程去处理,所以多路复用就是事件触发的机制。多路复用有3种实现:1.select采用1024大小的为数组存放文件描述符,调用select系统调用会把它拷贝到内核,内核遍历检测变化,再拷贝回用户区,用户再去遍历数组处理发生变化socket。所以他是2次拷贝,2次遍历。2.poll与select的区别就是他用动态数组,没有长度限制,但他俩开销都很大。3.epoll:调用epoll_create在内核创建epoll实例(结构体),里面有2个成员:红黑树、双链表。通过epoll_ctl把要检测的socket添加进红黑树进行检测,查找效率logn,很快,把发生变化的放入就绪链表,拷贝回用户区。epoll不需要每次检测都拷贝整个socket集合到内核区,且红黑树遍历更快,且直接拷贝发生改变的集合到用户区,用户不需要再遍历判断。
但并不是说epoll一定闭select/poll强,当连接数量少,且短连接较多,建议用select/poll,因为epoll每次添加一个文件描述符都要进行一次系统调用epoll_ctl,若短期连接较多触发频繁的系统调用,epoll性能可能会慢于他们;当监听的文件描述符较多,且长连接多,用epoll。

int epoll_create(1); //创建epoll实例,参数只要不是0就行

//向红黑树里add,del,mod文件描述符,mod:如监听读事件变为写事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

//检测红黑树里的这些事件,  timeout: -1表示一直等待,直到有事件发生。>=0表示等待指定时间
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll工作模式:LT模式(水平触发,默认模式)。当有可读事件,它会不断的通知你,直到你读完所有数据(如每次只能读5个字节)。ET模式(边沿触发):当有可读事件时,只会通知你一次,后续不通知了,除非又有新事件发生。
以快递为例:
水平触发+阻塞IO:驿站会一直给你短信,直到你把快递取掉,驿站才能干其他事。
边缘触发+阻塞IO:驿站只给你发一次短信,你把快递取掉之前,它不能做其他事。这种几乎不用。
水平触发+非阻塞IO:一直给你发,它照样干其他的事。
边缘触发+非阻塞IO:给你发一次,它去干其他事。
总结:ET一般配合非阻塞IO,用于高并发场景,减少开销,但要注意:最好一次性循环读取完所有数据(可根据read函数返回值)。
LT适用于精确控制场景,保证数据完整性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值