信号量
在Linux中,根据是否有唯一的名称,分为有名信号量和无名信号量。
无名信号量
无名信号量不是通过名称标识,而是直接通过sem_t结构的内存位置标识。
有名信号
sem_close():关闭对应sem指向的有名信号量的引用,每个打开了有名信号量的进程在结束时都应该关闭。
sem_unlink():移除内存中的有名信号量对象,/dev/shm下的有名信号量会被清除。当没有任何进程引用该对象时才会执行清除操作,只应该执行一次。
可用于进程间通信的方式通常都可以用于线程间通信。
无名信号量和有名信号量均可用于进程间通信,有名信号量是通过唯一的信号量名称在操作系统中唯一标识的。
无名信号量用于进程间通信时必须将信号量存储在进程间可以共享的内存区域,作为内存地址直接在进程间共享。而内存区域的共享是通过内存共享对象的唯一名称来实现的。
无名信号量和有名信号量都可以作为二进制信号量和计数信号量使用。
二进制信号量起到了互斥锁的作用,计数型信号量起到了控制进程或线程执行顺序的作用。
信号量是用来协调进程或线程协同工作的,本身并不用于传输数据。
简介
线程池是一种用于管理和重用多个线程的设计模式。
它通过维护一个线程池(线程的几何),可以有效处理并发任务而无需每次创建和销毁线程。这种方法可以减少线程创建和消耗的开销,提高性能和资源利用率。
Glib库线程池工作流程
- 线程池创建:首先创建一个线程池,指定任务函数和其他参数。线程池会创建一定数量的线程,这些线程进入等待状态,准备执行任务,或在提交任务后才创建线程(取决于配置)。线程池中所有任务执行的都是同一个任务函数。
- 任务队列:线程池维护一个任务队列。当我们向线程池提交任务时,任务会被放入这个队列中。实际上,放入任务队列的是我们在提交任务时传递的任务数据。
- 线程执行任务:线程池中的线程从任务队列中取出任务数据,然后调用任务函数,执行任务。执行完成后,线程不会退出,而是继续从任务队列中取下一个任务执行。如果没有待执行的任务,线程通常在等待一段时间后被回收(取决于具体的配置)。
void task_func(gpointer data, gpointer user_data){
int task_num = *(int *)data;
free(data);
printf("%d\n", task_num);
}
int main(){
GThreadPoll* pool = g_thread_pool_new(task_func, NULL, 5, TRUE, NULL);
//向线程池中添加任务
for(int i=0; i<10; i++){
//每一个提交任务的编号
int *tmp = malloc(sizeof(int));
*tmp = i + 1;
g_thread_pool_push(pool, temp, NULL);
}
g_thread_pool_free(pool, FALSE, TRUE);
}
跳跃游戏二
求跳到数组的最后所需的最小跳跃次数。
class Solution {
public:
int jump(vector<int>& nums) {
int maxPos = 0, n = nums.size(), end = 0, steps = 0;
for(int i=0; i<n-1; i++){
if(maxPos >= i){
maxPos = max(maxPos, i+nums[i]);
if(i == end){
end = maxPos;
steps++;
}
}
}
return steps;
}
};
外存循环log n,内存循环n,乘在一起O(nlogn)
OPT算法:是一种理想的页面置换算法,总是选择将来最长时间内不会访问的页面进行替换。
无法实现的原因:
预知未来: OPT算法需要预知未来哪些页面会被访问,这在实际系统中是不可能的。
理论模型: OPT算法是一种理论上的最佳算法,用于衡量其他算法的性能,而非实际应用。
字节填充是一种在数据链路层中使用的技术,为了防止数据中的特殊字节序列被误认为是帧的起始或结束标志,从而保证数据的完整性 。
左移运算是将一个数的二进制表示向左移动若干位,相当于乘以2的i次幂。
多态类中的虚函数表建立在编译阶段。
s是一个包含5个元素的数组,里面的每个元素都指向一个函数,函数返回值是void 类型,有一个int类型的参数。
函数指针数组
extern "c"实现C++代码调用其他C语言代码。
stdio.h文件中包含标准输入输出函数的函数声明,通过引用此文件以便能正确使用printf、scanf等函数。
转义字符是一种特殊的字符常量,以反斜杠\开头,后面跟一个字符或一个八进制数或一个十六进制数来表示ASCII码中不能直接表示的字符。
#include "file.h"是指编译器从当前工作目录上开始查找此文件。
构造函数可以是私有的
C++中空类默认产生的类成员函数有:
- 默认构造函数
- 析构函数
- 拷贝构造函数
- 赋值函数
- 取址运算符
- 常量取址运算符
跳表是一个非常好的数据结构,在Linux内核里面经常看见,它的插入、删除、查找的平均时间复杂度为O(log N)。
多线程读写同一个shared_ptr对象不需要加锁,因为shared_ptr对象本身是线程安全的。
假设有8台机器,有N个进程需要消耗2台这样的机器,规定每个进程每次只能申请一台,最多允许7个进程参与竞争,而不会发生死锁问题。
DNS完成域名到IP地址的映射,RARP实现MAC地址到IP地址的映射。
TCP协议有拥塞控制功能和流量控制功能。
视频流传输是UDP居多。
TCP建立连接需要3次握手,3次握手也可能对其造成flood攻击的可能。
TCP是HTTP、FTP的底层实现协议,UDP是TFTP的底层实现协议。
进程同步异步,和阻塞非阻塞没有直接关系。
进程和程序
程序是存储在硬盘等存储介质的代码,是一串二进制机器码,是静态的。
进程是正在运行的程序以及相关资源的总称,是一种抽象的,动态的。
加载程序
申请资源
启动进程
同一个程序可以对应N个进程。
进程控制块PCB
内核会保存每个进程的信息,称为进程控制块(Process Control Block)
- 进程编号(PID),每个进程对应唯一的编号,一般为正整数形式。
- 进程状态信息。
- 进程切换时需要保存和恢复的一些CPY寄存器,其中关键的有程序计数器的值,用于记录进程恢复时应执行的指令地址
- 内存管理信息。如页表、内存限制、段表等。
- 当前工作目录。
- 进程调度信息,包括进程优先级、调度队列指针等。
- I/O状态信息,包括分配给I/O设备列表,打开的文件描述符等,后者很多指向file结构体的指针。
- 同步和通信信息,包括信号量、信号等用于进程同步和通信机制的信息。
- 用户id和组id。
进程内存模型
栈
栈是FILO先进后出的数据结构,在进程的内存模型中有一块区域作为用户栈,是先进入的数据位于栈地,最后进入的数据位于栈顶。
栈帧是程序在调用函数时存储函数调用和局部变量的一块内存。
每次调用函数时,都会在调用者的栈上创建一个新的栈帧。这个栈帧包含了函数的返回地址、参数、局部变量以及保存的必要的寄存器值。
每个栈帧对应于一个函数调用的上下文,它是程序运行时栈内存组织的基本单元。栈帧允许函数调用彼此隔离,同时支持递归调用和嵌套使用。
调用函数时,会在栈顶为函数分配一帧,然后移动栈指针,指向新的栈顶,称为压栈。函数调用结束时,会释放函数的栈帧,称为弹栈。
栈指针
专用寄存器——栈指针(SP),用于记录栈顶位置。
帧指针
用于记录调用者栈帧的位置FP
内核空间
内核空间是进程虚拟内存中保留给操作系统内核的部分,用于存放内核数据和代码。这部分空间对用户不可见,不可以直接访问。内核空间有最高的访问权限,只有内核态下的代码可以执行这里的操作。
所有进程的内核空间是共享的。
用户空间
用户空间是内存中分配给用户程序的部分,与内核空间隔离。
用户程序和库函数在这里执行。用户空间的代码运行在用户态,具有较低的权限,不能直接执行特权操作或访问内核空间。
抽象的进程状态模型
进程状态是指一个进程在器生命周期中处于不同阶段或状态。
- 初始态:这是进程生命周期的开始阶段,进程被创建时处于初始态,这个阶段,操作系统为新进程分配资源。
- 就绪态:意味着进程已准备好运行,但由于CPU调度算法或其他正在运行的进程,它当前没有运行。
- 运行态:进程正在CPU上运行。
- 阻塞态:进程由于等待某个事件完成而无法继续执行时,处于阻塞态。
- 终止态:进程执行完毕,并释放其占用的所有资源,进行必要的清理工作。此时,虽然进程已结束了所有活动,但操作系统内核仍保留它的PCB,进程处于终止态。通常,终止态持续时间非常短暂,PCB很快会被内核释放。
- 僵尸态:僵尸态与终止态非常相似,区别是,如果进程结束了所有工作后PCB长期未被释放,它就处于僵尸态。在进程状态机中,我们对终止态和僵尸态不作区分,因为二者都是进程任务执行完毕之后的状态,意味着进程生命周期的终止。
调度队列
Linux的进程PCB底层数据结构是task_struct,操作系统将task_struct实例组织到不同的队列,以支持调度器的决策。
就绪队列
等待队列
终止队列
虚拟内存和物理内存
#include <stdio.h>
#include <unistd.h>
int main(){
int val = 123;
pid_t pid = fork();
if(pid == 0){
val = 321;
printf("%d的地址%p", val, &val);
}else if(pid > 0){
sleep(1);
printf("%d的地址%p", val, &val);
}
}
虚拟内存
虚拟内存是计算机系统内存管理的一种技术,它为每个进程提供了一种虚拟的地址空间,这个地址空间对于每个进程来说看起来是连续的,但实际上可能被分散地存储在物理内存和磁盘上。虚拟内存允许系统超额分配内存,即分配的内存总量可以超过物理内存的实际容量。
虚拟内存简化了内存管理,使得应用程序不需要关心物理内存的实际情况。
物理内存
物理内存指计算机中安装的RAM。它是系统用来存储正在运行的程序和数据的硬件资源。
物理内存直接影响到计算机能够同时处理的信号量。更多的物理内存意味着可以同时运行更多程序。
MMU
进程可以直接操作的只有虚拟内存,但虚拟内存毕竟是虚拟的,进程的代码段、数据、栈等最终一定要存储到真正的物理内存,那么就需要建立虚拟内存和物理内存之间的映射关系。
MMU是CPU的一个组成部分,负责处理虚拟地址到物理地址的转换。当程序试图访问一个虚拟内存地址时,MMU会查询页表找到对应的物理内存地址,完成内存访问。MMU还负责检查访问权限,确保程序不会访问未授权的内存区域。
此外,当请求的虚拟地址没有映射到物理地址或虚拟页对应的数据位于磁盘的交换空间中时,会产生缺页故障,MMU会通知操作系统,让操作系统为虚拟页分配页帧,或将数据从硬盘加载到内存的一个页中,然后更新页表建立新的映射关系。
MMU本身是硬件组件,操作系统负责配置和管理MMU使用的数据结构(如页表),以及处理MMU生成的各种内存管理相关的异常。
页是虚拟内存管理中的一个基本单位,通常大小为4KB或2MB等,具体大小依赖于处理器和操作系统的设计。操作系统使用页来实现虚拟内存。
页帧,也称为物理页。是物理内存中的一个固定大小的区块。在虚拟内存系统中,物理内存被划分为许多这样大小相等的页帧,以便于内存的管理和映射。
页表是操作系统用于管理虚拟内存系统中的虚拟地址到物理地址映射的数据结构。页表包含页表项(Page Table Entries, PTEs),每个PTE对应一个页,包含该页映射到的页帧的物理地址及访问该页的权限和状态(如是否在物理内存中,是否可写等)。在x86-64架构下,页表大小为4K。
共享内存
- 通过shm_open创建共享内存对象,这一步实际上是内核建立了一个匿名文件,它不与文件系统的具体文件直接关联,只是存在于内核的一个对象。内存共享对象是通过文件名来唯一标识的。
- 通过mmap()将进程的虚拟内存映射到共享对象,当多个进程映射到相同的共享对象时,内核会根据文件名确定,并将多个进程的虚拟内存映射到相同的物理内存,实现进程间内存共享。
内核态和用户态
内核态是CPU的一种运行模式,具有执行所有指令和访问所有硬件资源的权限。在这种模式下,操作系统内核执行其核心功能。所有与硬件交互的操作系统必须在内核态下执行。
由于具有完全的系统控制权,任何在内核态执行的代码都必须是高度可靠的,以避免系统崩溃或安全漏洞。
用户态是CPU的另一种运行模式,权限受限。应用程序在用户态下运行,不能直接执行特权指令或访问受保护的内存区域。
用户态为应用程序提供了一个安全的执行环境,通过系统调用请求操作系统提供的服务。
特权指令是指只有在内核态下才可以执行的指令。这些指令提供了对硬件和关键系统资源的直接控制能力,因此它们的执行被严格限制在操作系统内核中,以防止恶意软件或错误的程序代码破坏系统的稳定性和安全性。
中断和异常
中断通常是I/O设备或时钟触发的,信号来自处理器外部,不是由任何一条指令造成的,从这个角度讲,它是异步的。中断处理完毕后总是执行下一条指令。
异常是CPU执行指令时检测到特定条件触发的。
x86-64定义了三种异常:陷入、故障和终止。
- 陷入:是由进程执行陷入指令(可以切换到内核态的指令)主动触发的,是同步的,执行完后总是执行下一条指令。
- 故障:故障是由错误情况引起,可能被故障处理程序修正,如上文提到的缺页故障,故障发生时,处理器将控制权交由故障处理程序,如果故障被修复,则返回引起故障的指令,并重新执行。否则,处理程序返回到内核中的abort例程,后者终止引起故障的进程。
- 终止是不可恢复的致命错误造成的结果。如底层硬件错误,或者进程产生的算数异常,无法被修复。终止发生时,CPU将控制权交由终止处理程序,这个程序不会将CPU的控制权返还给应用程序,而是返回到内核中的abort例程,后者终止进程。