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

Linux进程间通信——system V信号量

文章目录

  • system V信号量
    • 一些前置了解概念
      • 临界资源 & 共享资源
      • 原子性
      • 临界区和非临界区 & 锁
      • 同步和互斥
    • 信号量的本质和理解
      • 信号量的P、V操作
      • 二元信号量
      • 信号量与通信的关系
      • 信号量集结构体
    • 信号量相关接口
      • semget
      • semctl
        • 初始化工作
        • 删除信号量集
      • semop
    • 代码实例——基于建造者模式 & system V信号量实现锁

system V信号量

在之前,我们已经学习过system V版本的共享内存了。了解了进程间是如何通过共享内存的方式进行通信的。

但是,system V是一种标准。在这种标准下,其实还存在其它的进程间通信。接下来我们将重点关注system V版本下的信号量。了解信号量基本概念和基本操作。这便于后序学习多线程。

一些前置了解概念

在讲解信号量的相关内容前,我们需要来了解一些概念,以便我们能够更好地学习。

我们曾经讲过一个基本的结论:
即进程间通信的大前提是:进程需要看到同一个位置的同一份资源!
而共享内存不像管道那样,带有同步机制。这就导致写方还没写完,读方就已经在读了。这本质是没有对数据进行保护,也就是没有数据保护机制

所以,为了解决这个问题,system V标准下引入了一个信号量。信号量能有效解决这个问题。

临界资源 & 共享资源

在多个执行流/多个进程能够看到的资源,就叫做共享资源。
(就如共享内存一样,是可以让两个进程都看到这个空间的。)


而被保护起来的资源,就叫做临界资源!

原子性

我们经常能够看到说某部分内容实现了原子性操作。那么,这个原子性到底是什么?
其实,原子性就是指一个操作要么完全执行,要么完全不执行,不会被中断或部分完成。​

就好比电梯开关门,要不然完全打开,要不然完全关闭。没有中间状态

本质上,共享内存通信方案就不是原子性的。如果一个通信方案要满足原子性:
那么读方要么不读,要么触发了特定条件才读取。是没有其余的中间状态的。

所以,我们才会使用管道对共享内存通信的两个进程进行同步机制的应用的。

临界区和非临界区 & 锁

在代码中,涉及到保护资源的代码(如对保护资源读、写)等,统称为临界区!其余非临界区!

一般来说,为了保证临界区(共享资源所在处)的原子性,在执行代码的时候,从非临界区进入临界区是需要加锁的。 如下图所示。

在这里插入图片描述
因为要保证数据的安全(不能出现没写完就进行读取)。所以需要保证,访问临界区代码的时候,是原子性的!也就是需要满足一定条件才能让读取方进行读取。

所以,就需要在临界区和非临界区中间加上一把锁。这个锁就是用来保证原子性的。进程想要访问临界区,需要先申请锁,如果锁的数量不够了,那么就需要等待,直到有锁(其余进程不访问临界区后会进行解锁操作)。


但是,对于锁来说,也是一种资源。而且也是能够让多个进程看到的资源。
-> 所以锁本身也就是共享资源!那么,如何保证锁也是安全的呢?即不会被错误申请。
这就需要使申请锁的机制变成原子性的!

这样子就可以保证锁的安全了。

同步和互斥

同步和互斥的概念主要是代码执行流方面的。
其实,对于共享资源的保护,就可以分为同步和互斥两种方案:

1.访问共享资源的所有进程,按照一定的顺序流进行访问 -> 同步
2.任何时刻,只能有一个执行流访问资源 -> 互斥(如银行ATM取钱)

上述的两种方案都可以起到保护共享资源的作用,只不过原理不相同。

在本部分学习的信号量中,我们将重点介绍如何信号量来充当锁的做法。即我们更多的是使用锁互斥的方式来进行资源保护。信号量其实也可以使用同步的方式来保护资源。

信号量的本质和理解

先说结论:信号量,本质就是一个资源计数器!


举一个例子:
就好比一个电影院内的放映厅,里面有若干个座位。
我们买票,其实就是在预定那个时间段的座位。如果座位余量还剩,那么我们就可以买票。要是座位剩余量还剩0个,也就是卖完了,那就没办法再购买。

而且,电影院买票,本质上就是对资源的预定!因为我们只要买了电影票,那个时间段的座位就是我们的。别人是不能动的。


转换到各个进程访问共享资源的时候:
信号量 : 共享资源的份数 -> 电影余票量
申请信号量 :预定访问共享资源 -> 预定座位

其实信号量就是指:某个访问资源的剩余份数。即我们需要把某分资源分成多少份。
所有的进程,在访问前,都需要申请信号量!(信号量 > 0)

假设信号量是10份 -> 把共享资源分成十份使用:
如果信号量不为0,说明共享资源还能用,那么就可以让进程拿到这个信号量,作为访问共享资源的门票。然后,信号量需要减少一个。访问完共享资源/不用了,就需返还信号量,信号量就要加1个。


如果信号量剩余数为0,那么就代表此时访问共享内存的情况是不允许再有进程进行访问。所以,该进程就需要阻塞在申请信号量的地方,等待信号量不为0的时候才能继续访问!

信号量的P、V操作

信号量本身就是一个指向共享资源的访问次数的计数器:
进程拿到信号量,信号量-1,即P操作。
进程返还信号量,信号量+1,即V操作。

但是,信号量本身,也是共享资源。所有进程也是会访问到信号量的!所以,如果信号量只是单纯的一个简单计数器,那么是不能保证信号量本身是原子性的!也就会导致数据不安全。

所以,信号量必然不是一个简单的计数器,它的P、V操作也不可能是简单地对计数器做±1的操作,其P、V操作必然也是要原子的!

当然,这里不过多深究信号量本身的机制是如何保障原子性的。我们只要知道是原子性的,并且能够学会并且运用就可以了!

二元信号量

如果把一份共享资源分成N份使用,那么信号量就是N。但是如果信号量的值为1呢?
信号量为1的信号量,被称为二元信号量

如果是二元信号量 -> 一份资源只能同时让一个执行流进行访问 -> 充当锁!

其实就是:
如果把资源进行整体使用,那么信号量就是1。反之信号量 >1。
(电影院放映厅就是把资源分成多分来使用。)

信号量与通信的关系

当信号量 = 0的时候,进程就不能再访问共享资源,需要等到信号量被返还后,按照申请的顺序接收到了信号量后,才能继续进行访问。
那么,当进程没有接收到信号量的时候,就需要把这些进程变成阻塞状态(从运行队列转换到阻塞队列中)。

从上述我们可以直到,信号量虽然不直接参与进程间数据的通信,但是它是控制进程能否访问共享资源的!也就是信号量起到的是通知的作用!又或是以同步/互斥的方式对进程进行通信控制,这本质上也是属于进程通信的一种!

信号量集结构体

其实,在系统中,信号量被称为信号量集!因为我们可以一次性申请多个信号量。但是这所有的信号量都是一样的semid值。为了区分信号量的不同,会在集合内部进行编号!

The semid_ds data structure is defined in <sys/sem.h> as follows:
struct semid_ds {struct ipc_perm sem_perm; /* Ownership and permissions */time_t sem_otime; /* Last semop time */time_t sem_ctime; /* Last change time */unsigned long sem_nsems; /* No. of semaphores in set */
};///////////////////////////////////////////////////////////////////////////////The ipc_perm structure is defined as follows (the highlighted fields
are settable using IPC_SET):
struct ipc_perm {key_t __key; /* Key supplied to semget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions */unsigned short __seq; /* Sequence number */
};

我们会发现,和描述共享内存的描述方式也是一样的。因为它们都属于system V标准!
又因为系统中必然会存在多个信号量集,不同的信号量集有不同的操作,不同的状态,所以操作系统需要以一定的方式组织这些信号量集。

方法永远都是:先描述,再组织!

信号量相关接口

接下来是信号量(semaphore)的相关接口

semget

NAMEsemget - get a System V semaphore set identifierSYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semget(key_t key, int nsems, int semflg);RETURN VALUEIf successful, the return value will be the semaphore set identifier
(a nonnegative integer), otherwise, -1 is returned, with errno indicating the
error.

这个接口也是和共享内存获取接口shmget一样的,需要通过外部约定的key来标识不同的信号量集。(因为都是system V标准)

key的生成一般都是使用函数ftok即可。这里不再赘述。
返回值和shmget的说法是一样的,成功返回一个标识符id,反之返回-1并设置错误码。

semflg选项也是和shmget一样
shmflg -> 由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的:
取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。 取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,出错返回。
创建内存的时候也会有权限的问题,所以可以在选项里面叠加权限码,如:IPC_CREAT | 0666

这里需要说明一个不一样的就是:
参数nsems,这表示当前申请的信号量集的个数。即几个集合内有几个信号量!
这个参数需要根据不同的具体场景来进行设置!

semctl

这个接口即semaphore control,信号量集的控制。

NAMEsemctl - System V semaphore control operations
SYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semctl(int semid, int semnum, int cmd, ...);This function has three or four arguments, depending on cmd. When there are
four, the fourth has the type union semun. The calling program must define
this union as follows:union semun {int val; /* Value for SETVAL */struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */unsigned short *array; /* Array for GETALL, SETALL */struct seminfo *__buf; /* Buffer for IPC_INFO(Linux-specific) */
};RETURN VALUEOn failure, semctl() returns -1 with errno indicating the error.Otherwise, the system call returns a nonnegative value depending on
cmd as follows:GETNCNT the value of semncnt.GETPID the value of sempid.GETVAL the value of semval.GETZCNT the value of semzcnt.IPC_INFO the index of the highest used entry in the kernel's internal
array recording information about all semaphore sets. (This information can
be used with repeated SEM_STAT orSEM_STAT_ANY operations to obtain information about all semaphore sets
on the system.)SEM_INFO as for IPC_INFO.SEM_STAT the identifier of the semaphore set whose index was given in
semid.SEM_STAT_ANY as for SEM_STAT.GETALL Return  semval  (i.e., the current value) for all semaphores of the set into arg.array.  The argument semnum is ignored.  The calling process must have read permission onthe semaphore set.GETNCNTReturn the semncnt value for the semnum-th semaphore of the set (i.e., the number of processes waiting for the semaphore's value to increase).  The calling  process  musthave read permission on the semaphore set.GETPID Return  the  sempid value for the semnum-th semaphore of the set.  This is the PID of the process that last performed an operation on that semaphore (but see NOTES).  Thecalling process must have read permission on the semaphore set.GETVAL Return semval (i.e., the semaphore value) for the semnum-th semaphore of the set.  The calling process must have read permission on the semaphore set.GETZCNTReturn the semzcnt value for the semnum-th semaphore of the set (i.e., the number of processes waiting for the semaphore value to become 0).   The  calling  process  musthave read permission on the semaphore set.SETALL Set  the  semval values for all semaphores of the set using arg.array, updating also the sem_ctime member of the semid_ds structure associated with the set.  Undo entries(see semop(2)) are cleared for altered semaphores in all processes.  If the changes to semaphore values would permit blocked semop(2) calls in other processes to proceed,then those processes are woken up.  The argument semnum is ignored.  The calling process must have alter (write) permission on the semaphore set.SETVAL Set  the semaphore value (semval) to arg.val for the semnum-th semaphore of the set, updating also the sem_ctime member of the semid_ds structure associated with the set.Undo entries are cleared for altered semaphores in all processes.  If the changes to semaphore values would permit blocked semop(2) calls in other processes  to  proceed,then those processes are woken up.  The calling process must have alter permission on the semaphore set.All other cmd values return 0 on success.

基本操作还是和shmctl类似的,同时需要通过指定的id来进行相关操作。

初始化工作

信号量和共享内存还不太一样,我们需要对信号量进行一些初始化工作。
比如信号量的值是多少。初始化工作需要通过一个联合体来做:

union semun {int val; /* Value for SETVAL */struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */unsigned short *array; /* Array for GETALL, SETALL */struct seminfo *__buf; /* Buffer for IPC_INFO(Linux-specific) */
};

这个联合体,需要在使用的时候自行写到代码中使用,使用的库是没有包含这个联合体的。

一般来说,我们都是初始化信号量的值得,即联合体内的变量int val
同时,还需要给定选项SETVAL传给参数cmd,将联合体变量传递给后序的可变参数接收

但是这还没完,信号量集虽然可以一次性创建多个信号量,但是,其初始化操作一次只能做一个!这也就是semnum的作用。
一个信号量集开辟 -> 会从0开始对信号量进行编号 -> semnun就是要被初始化的那个信号量对应的编号

所以,如果要对一个信号量集进行初始化操作,就必须要根据上述的步骤,将一个信号量集中所有的信号量一一进行初始化!这是信号量集初始化时候的缺陷!

删除信号量集

删除信号量集就不必多说了,和shmctl的删除操作一样:
cmd 接收选项 IPC_RMID
semid 接收信号量集对应的标识符id

但是,信号量集删除,是把一个集内的所有信号量全删除了!那么,对于删除的时候,semnum怎么办呢?
答案:删除的时候,这个参数是没有用的,可以被忽略:
在这里插入图片描述
所以这个参数随便传就可以了。可变参数也是不用传内容的!

semop

semop,即semphore operate,这个是用来对信号量集做操作的!
共享内存是通关shmat和shmdt来实现进程地址空间和物理地址空间的联系的。然后就可以通过一个地址进行通信。

这里信号量的相关操作(如P,V),都是通过semop来进行的。这个过程是原子的!

NAMEsemop, semtimedop - System V semaphore operations
SYNOPSIS#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>int semop(int semid, struct sembuf *sops, size_t nsops);semop() performs operations on selected semaphores in the set indicated by
semid. Each of the nsops elements in the array pointed to by sops is a 
structure that specifies an operation to be performed on a single semaphore.
The elements of this structure are of type struct sembuf, containing the
following members:unsigned short sem_num; /* semaphore number */short sem_op; /* semaphore operation :-1,P操作。1,V操作*/short sem_flg; /* operation flags */Flags recognized in sem_flg are IPC_NOWAIT and SEM_UNDO. If an
operation specifies SEM_UNDO, it will be automatically undone when the
process terminates.RETURN VALUEIf successful, semop() and semtimedop() return 0; otherwise they
return -1 with errno indicating the error.

第一个参数就是信号量集对应的标识符id。这个不需要解释。

但是第二个和第三个参数是什么?
其实是这样的,这个接口是可以一次性对一个集内的多个信号量进行操作的。
只不过是,我们需要先设定好每个信号量需要的操作,设定方法通过一个结构体:

struct sembuf{unsigned short sem_num; /* semaphore number */short sem_op; /* semaphore operation :-1,P操作。1,V操作*/short sem_flg; /* operation flags */
};

结构体内第一个变量是一个信号量集内被操作的信号量的编号
第二个变量是对信号量的操作:-1,P操作;1,V操作
第三个参数sem_flg是操作的选项,这个我们一般不需要管,直接给0就可以了。

第三个参数是信号量操作的个数。
其实结合第二个参数我们就大概直到是什么意思了,其实就是一个结构体数组!
我们把操作的信号量的相关信息设定在结构体内(信号量集内一个信号量对应一个结构体),然后把若干个结构体放在一个数组内,再传递数组的大小就可以做到一次性操作多个信号量了!

当然,如果只是操作一个信号量的话,那就不需要数组也是可以的!

代码实例——基于建造者模式 & system V信号量实现锁

接下来,我们可以通过一些代码来进行上面内容的复习和巩固:
获取链接:https://gitee.com/yangnp/linux-learn-code/tree/master/2025_8_1/semaphore_v2

这个代码是基于基础版本修改过来的。基础版本在2025_8_1/semaphore_v1下。

Sem_v2.hpp

#pragma once
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <string>
#include <memory>
#include <iostream>
#include <vector>
#include <errno.h>
#include <sys/wait.h>using namespace std;int default_perm = 0666;
string path(".");
int PROJ_ID = 0x7866;#define GET_SEM IPC_CREAT
#define CREATE_SEM (IPC_CREAT | IPC_EXCL)//基于创造者模式进行结构的创建//具体的操作 -> 产品做
class Semaphore{
private:void PV(int who, int opr){//因为当前信号量集二元,所以内部只有一个信号量,编号为0struct sembuf _sem_buf;_sem_buf.sem_flg = 0;_sem_buf.sem_op = opr;_sem_buf.sem_num = who;int semop_rd = semop(_semid, &_sem_buf, 1);if(semop_rd < 0){cerr << "semop error!" << endl;exit(errno);}}
public:Semaphore(int semid, int flag):_semid(semid), _flag(flag){}//用户决定申请编号为who的信号量void P(int who){PV(who, -1);}void V(int who){PV(who, 1);}~Semaphore(){if(_flag == GET_SEM) return;cout << "it's going to remove sem!" << endl;int rm_sem_id = semctl(_semid, 0, IPC_RMID);if(rm_sem_id < 0){cerr << "remove sem error!" << endl;exit(errno);}else cout << "remove sem success!" << endl;}
private:int _semid;int _flag;
};//只写出建造的方法,让真正的创建者Bulid进行虚函数重写,实现指定创建方式
class Bulid_Opeator{
public:virtual void GetKey() = 0;virtual void GetPerm(int perm) = 0;virtual void GetNsems(int nsems) = 0;virtual void GetFlag(int flag) = 0;virtual void GetNsemsVal(vector<int> initval_vector) = 0; // 将信号量集中每个编号(从0开始)的值放在这个vector中virtual void Bulid(int flag) = 0;virtual void Init() = 0;};//继承建造方法 完成产品创建的各类方法
class SemaphoreBulider : public Bulid_Opeator{
public:SemaphoreBulider() = default;virtual void GetKey() override{_key = ftok(path.c_str(), PROJ_ID);if(_key < 0) {cerr << "ftok error!" << endl;exit(errno);}else cout << "ftok success! key : " << _key << endl;}virtual void GetPerm(int perm) override{_perm = perm;}virtual void GetNsems(int nsems) override{_nsems = nsems;}virtual void GetFlag(int flag) override{_flag = flag;}virtual void GetNsemsVal(vector<int> initval_vector) override{_initval_vector = initval_vector;}virtual void Bulid(int flg) override{_semid = semget(_key, _nsems, flg);if(_semid < 0){cerr << "semget error!" << endl;exit(errno);}else cout << "semget success!" << endl;}virtual void Init() override{if(_flag == GET_SEM) return;// 此时就不是单纯的对信号集中某一个编号的信号量进行初始化,而是对所有的进行初始化// 初始化的值就放在了_initval_vector数组中union semun {int              val;    /* Value for SETVAL val > 0*/struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */unsigned short  *array;  /* Array for GETALL, SETALL */struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux-specific) */}un;for(int i = 0; i < _nsems; ++i){un.val = _initval_vector[i];int semctl_rd = semctl(_semid, i, SETVAL, un);if(semctl_rd < 0){cerr << "sem init(SETVAL) error!" << endl;exit(errno);}else cout << "sem init(SETVAL) success!" << endl;}}// 获取到一个指向信号量集的智能指针 直接就可以使用shared_ptr<Semaphore> outer_get_semaphorePtr(){return make_shared<Semaphore>(_semid, _flag);}private:key_t _key;int _perm;int _nsems;int _semid;int _flag;vector<int> _initval_vector;
};//指挥官 -> 指挥builder造出产品
class Semaphore_Director{
public:Semaphore_Director(){}~Semaphore_Director(){}void Organize_Bulid(SemaphoreBulider& sem_bulider, int flag, int perm = 0666, int nsems = 1, vector<int> initval = {1}){sem_bulider.GetKey();// 一定要先设定权限值sem_bulider.GetPerm(perm);sem_bulider.GetNsems(nsems);sem_bulider.GetFlag(flag);sem_bulider.GetNsemsVal(initval);sem_bulider.Bulid(flag | perm);sem_bulider.Init();}shared_ptr<Semaphore> Get_SemaphorePtr(SemaphoreBulider& sem_bulider){return sem_bulider.outer_get_semaphorePtr();}private:
};

这里就对代码进行一些基础的解释:

首先,建造者模式是一种创建型设计模式,它允许你分步骤创建复杂对象。这种模式特别适用于需要创建包含多个组成部分的对象,且这些部分需要按特定顺序组装的情况。

核心思想

  • ​​分离构造与表示​​:将对象的构造过程与对象本身的表示分离
  • ​​分步构建​​:通过一步步的构建过程来创建复杂对象 ​- ​统一构建接口​​:提供统一的构建接口,可以创建不同的对象表示

主要角色

  • ​​Builder(建造者)​​:定义创建产品各个部件的抽象接口
  • ​​ConcreteBuilder(具体建造者)​​:实现Builder接口,提供具体构建步骤
  • ​​Director(指挥者)​​:负责使用Builder接口来构建对象
  • ​​Product(产品)​​:最终要构建的复杂对象

所以,代码样例中:
信号量集Semaphore就是Product(产品),它负责做一些产品的相关操作,如信号量的P和V操作。

Builder(建造者)​是定义各个部件的抽象接口,即代码中的Bulid_Opeator
ConcreteBuilder(具体建造者)根据抽象接口来实现产品对应的创建方法,其实就是对Builder(建造者)中的抽象接口进行虚函数重写!如代码中SemaphoreBulider
最后是 ​​Director(指挥者),负责指挥创建产品。即可以通过ConcreteBuilder(具体建造者)具体实现的接口来进行指挥创建Product!如代码中Semaphore_Director


上述的代码中,就是根据建造者模式来模拟实现信号量的锁。其中,我们把信号量集的析构交给了信号量集本身对应的变量来做。即在class Semaphore的析构函数内。

同时,我们通过智能指针shared_ptr的使用,在建造者内定义了一个接口,可以获取到一个指向Product的智能指针!这样子,就可以让智能指针自动释放资源了!

具体的实现细节在这里就不多说了,其实就是对接口的基本使用,我们通过一份代码来验证:

#include "Sem_v2.hpp"
int main(){int cnt = 3;Semaphore_Director sem_direct;//父进程用的SemaphoreBulider bulider;sem_direct.Organize_Bulid(bulider, CREATE_SEM, 0660, 3, {1,2,3});auto fsem = sem_direct.Get_SemaphorePtr(bulider);pid_t subid = fork();if(subid < 0){cerr << "fork error!" << endl;exit(errno);}else if(subid == 0){//子进程用SemaphoreBulider bd;sem_direct.Organize_Bulid(bd, GET_SEM, 0660, 3, {1,2,3});auto csem = sem_direct.Get_SemaphorePtr(bd);while(cnt--){csem->P(0);printf("%c", 'C');fflush(stdout);printf("%c", 'C');fflush(stdout);sleep(1);csem->V(0);}exit(0);}else{while(cnt--){usleep(1500);fsem->P(0);printf("%c", 'F');fflush(stdout);printf("%c", 'F');fflush(stdout);sleep(1);fsem->V(0);}waitpid(subid, nullptr, 0);cout << endl;}return 0;
}

运行结果:

ftok success! key : 1711360664
semget success!
sem init(SETVAL) success!
sem init(SETVAL) success!
sem init(SETVAL) success!
ftok success! key : 1711360664
semget success!
CCFFCCFFCCFF
it's going to remove sem!
remove sem success!
http://www.dtcms.com/a/311677.html

相关文章:

  • linux 启动流程?
  • C++刷题 - 7.27
  • 深度学习-模型初始化与模型构造
  • 元宇宙重构未来交通新图景
  • 对过去一年毕业求职季的简单复盘
  • Gossip 协议
  • 锁相关(AI回答)
  • LeetCode Hot 100:3. 无重复字符的最长子串
  • 学习日志25 python
  • Vue3核心语法基础
  • FFmpeg+javacpp中纯音频播放
  • yolo 、Pytorch (5)IOU
  • 衡石科技实时指标引擎解析:如何实现毫秒级响应万亿级数据的增量计算?
  • 防御综合实验
  • TypeScript03-web项目知识
  • Python正则表达式使用指南:从基础到实战
  • 【C语言】内存函数与数据在内存中的存储
  • 自动驾驶中的传感器技术15——Camera(6)
  • 【数据结构初阶】--排序(二)--直接选择排序,堆排序
  • 内核协议栈源码阅读(三) --- 网桥处理
  • 每日五个pyecharts可视化图表-bars(1)
  • AG32mcu通过寄存器方式操作cpld
  • linux ssh公钥移除办法
  • K8S部署ELK(三):部署Elasticsearch搜索引擎
  • accept函数及示例
  • CMake指令:mark_as_advanced
  • Django 日志配置详解
  • gbase8s 常见表约束介绍
  • 数字化转型驱动中小制造企业的质量管理升级
  • 技术面试知识点详解 - 从电路到编程的全栈面经