进程间通信方式---System V IPC信号量

进程间通信方式—System V IPC信号量

文章目录

  • 进程间通信方式---System V IPC信号量
    • 信号量
      • 1.信号量原语
      • 2.`semget` 系统调用
            • 参数
            • 返回值
      • 3.`semop` 系统调用
            • 参数
            • 返回值
      • 4.`semctl` 系统调用
      • 5.特殊键值 `IPC_PRIVATE`
      • 6.信号量实现进程间通信
        • 1. 数据结构定义
        • 2. 信号量操作相关部分
        • 3. 生产者逻辑
        • 4. 消费者逻辑
        • 5. 主函数部分
        • 运行结果:
      • 7.无血缘关系进程通信

信号量

1.信号量原语

多个进程同时访问系统上某个资源时,如同时写一个数据库的某条记录,或同时修改某个文件,就需要考虑进程同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。通常,进程对共享资源的访问的代码只是很短的一段**,但这段代码引发了进程之间的竞态条件,我们称这段代码为关键代码区,或临界区,对进程同步,就是确保任一时刻只有一个进程能进入关键代码段。**

Dekker算法和Peterson算法试图从语言本身(不需要内核支持)解决进程同步问题,但它们依赖于忙等待,即进程要持续不断地等待某个内存位置状态的改变,这种方式的CPU利用率太低,不可取。

Dijkstra提出的信号量(Semaphore)是一种特殊的变量,它只能取自然数值且只支持两种操作:等待(wait)和信号(signal)。但在Linux/UNIX中,等待和信号都已经具有特殊含义,所以对信号量的这两种操作更常用的称呼是P、V操作,这两个字母来自荷兰语单词passeren(传递,就好像进入临界区)和vrijgeven(释放,就好像退出临界区)。

假设有信号量SV,对它的P、V操作含义如下:

  • P(SV),如果SV的值大于0,就将它减1,如果SV的值为0,则挂起进程的执行。
  • V(SV),如果有其他进程因为等待SV而挂起,则唤醒之,如果没有,则将SV加1。

信号量的取值可以是任何自然数,但最常用的、最简单的信号量是二进制信号量,它只能取0或1两个值,我们仅讨论二进制信号量。使用二进制信号量同步两个进程,以确保关键代码段的独占式访问的例子:

img

上图中,当关键代码段可用时,二进制信号量SV的值为1,进程A和B都有机会进入关键代码段,如果此时进程A执行了P(SV)操作将SV减1,则进程B再执行P(SV)操作就会被挂起,直到进程A离开关键代码段,并执行V(SV)操作将SV加1,关键代码段才重新变得可用。

不能使用普通变量来模拟二进制信号量,因为所有高级语言都没有一个原子操作可以同时完成以下两步操作:检测变量是否为true/false,如果是则将它设置为false/true。

Linux信号量的API定义在sys/sem.h头文件中,主要包括3个系统调用:semgetsemopsemctl。它们被设计为操作一组信号量,即信号量集,而不是单个信号量。

2.semget 系统调用

semget系统调用创建一个新的信号量集,或获取一个已经存在的信号量集。

#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
参数

key:键值,用来标志全局唯一的信号量集,要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量。

num_sems:指定要创建/获取的信号量集中信号量的数目,如果是创建信号量,该值必须指定,如果是获取已经存在的信号量,该值可以设置为0。

sem_flags:指定一组标志,低端的9个bite是信号量的权限,格式和含义与openmode参数一致。此外,它可以和IPC_CREAT标志做按位或运算以创建新的信号量集。还可以联合使用IPC_CREATIPC_EXCL标志确保创建新的、唯一的信号量集,如果这时候该信号量集已经存在,semget返回错误并设置errno为EEXIST

返回值

semget成功返回一个正整数,也就是信号量集的标识符,失败返回-1并设置errno。

如果用semget创建一个新的信号量集,与之相关的内核数据结构体semid_ds将被创建并初始化。

struct semid_ds {
	struct ipc_perm sem_perm;		/* 信号量操作权限 */
	unsigned long int sem_nsems;	/* 该信号量集中的信号量数目 */
	time_t sem_otime;				/* 最后一次调用 semop 的时间 */
	time_t sem_ctime;				/* 最后一次调用 semctl 的时间 */
                                    /* 省略其他填充字段 */
}:
 
struct ipc_perm{
	key_t key;						/* 键值 */
	uid_t uid;						/* 所有者的用户id */
	gid_t gid;						/* 所有者的组id */
	uid_t cuid;						/* 创建者的用户id */
	git_t cgid;						/* 创建者的组id */
	mode_t mode;					/* 访问权限 */
                                	/* 省略其他填充字段 */
};

img

3.semop 系统调用

semop系统调用改变信号量的值,即执行P、V操作,在讨论semop函数前,先介绍与每个信号量关联的一些重要的内核变量:

unsigned short semval; 			/* 信号量的值 */
unsigned short semzcnt; 		/* 等待信号量变为0的进程数量 */
unsigned short semncnt; 		/* 等待信号量值增加的进程数量 */
pid_t sempid; 					/* 最后一次执行 semop 操作的进程ID */

semop函数对信号量的操作实际就是改变上述内核变量的操作,该函数定义如下:

#include <sys/sem.h>
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
参数

sem_idsemget调用返回的信号量集标识符,指定被操作的目标信号量集。

sem_ops:指向一个 sembuf 类型结构体的数组:

struct sembuf
{
	unsigned short int sem_num;
	short int sem_op;
	short int sem_flg;
};
  • sem_num:信号量集中信号量的编号,0代表信号量集的第一个信号量,以此类推。
  • sem_op:指定操作类型,可选值:正整数,0、负整数,同时受到 sem_flg的影响。op>0执行V操作,op小于0执行P操作
    • 通常P操作值为 - 1,V操作值为 1
  • sem_flg:可选值为IPC_NOWAIT,SEM_UNDO,0
    • IPC_NOWAIT代表非阻塞操作SEM_UNDO代表撤销操作0代表阻塞操作
    • sem_flg设置为IPC_NOWAIT时,如果信号量操作(如sem_op中的减法操作使得信号量的值小于 0)不能立即执行,操作不会阻塞等待信号量状态改变,而是立即返回一个错误,错误码通常为EAGAIN。这种方式适用于不希望进程在信号量操作上长时间阻塞的场景,例如在一些对实时性要求较高的应用中,当获取不到信号量时可以先去执行其他任务。
    • sem_flg设置为SEM_UNDO时,系统会记录信号量操作,以便在进程异常终止时自动撤销(调整)信号量的值,以避免信号量状态被错误地锁定或者资源无法释放的情况。例如,如果一个进程对信号量进行了P操作(减操作)获取资源后异常终止,没有来得及进行V操作(加操作)释放资源,设置了SEM_UNDO的信号量系统会自动进行适当的调整,保证信号量状态的正确性。
    • sem_flg为 0 时,信号量操作会按照正常的阻塞方式执行。对于P操作(sem_op为负数),如果信号量的值不够减,进程会被阻塞,直到信号量的值满足操作要求(例如其他进程进行了V操作增加了信号量的值)。这种方式在需要确保资源按照顺序被访问和操作,且允许进程等待资源可用的场景下非常有用,比如在经典的生产者 - 消费者模型中,消费者进程等待生产者生产出产品(通过信号量控制),此时使用阻塞式操作可以保证消费者在没有产品时等待,直到生产者生产出产品后再继续执行。

num_sem_ops:指定要执行的操作个数,即sem_ops数组中元素的个数。semop函数对sem_ops数组参数中的每个成员按数组顺序依次执行操作,且该过程是原子操作,以避免别的进程在同一时刻按不同顺序对该信号集中的信号量执行semop函数导致的竞态条件。

返回值

semop成功返回0,失败返回-1并设置errno。

sem_op值的不同操作规则:

  • sem_op 大于 0 时,表示进程要增加信号量的值。操作要求调用进程对被操作信号量集拥有写权限。若设置了 SEM_UNDO 标志,系统将更新进程的 semadj 变量。
  • sem_op 等于 0 时,表示这是一个 “等待 0” 操作。操作要求调用进程对被操作信号量集拥有读权限。如果信号量的值为 0,调用立即成功;如果不是 0,则操作失败或阻塞进程直到信号量变为 0。在这种情况下,当 IPC_NOWAIT 标志被指定时,操作立即返回一个错误,并设置 errnoEAGAIN。若未指定 IPC_NOWAIT 标志,信号量的 semncnt 值加 1,进程将被投入睡眠直到满足特定条件。
  • sem_op 小于 0 时,表示对信号量值进行减操作,即期望获得信号量。操作要求调用进程对被操作信号量集拥有写权限。如果信号量的值 semval 大于或等于 sem_op 的绝对值,操作成功,调用进程立即获得信号量,并且系统将该信号量的 semval 值减去 sem_op 的绝对值。若设置了 SEM_UNDO 标志,则系统将更新进程的 semadj 变量。

4.semctl 系统调用

semctl系统调用允许调用者对信号量进行直接控制:

#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);

参数:

  • sem_id参数是由semget调用返回的信号量集标识符,用于指定被操作的信号量集。
  • sem_num参数指定被操作的信号量在信号量集中的编号。
  • command参数指定要执行的命令,有些命令需要调用者传递第 4 个参数。

第四个参数可以自定义,但是系统给出了推荐的定义格式:

union semun
{
    int val;// 用于SETVAL命令
    struct semid_ds *buf;// 用于IPC_STAT和IPC_SET命令
    unsigned short *array;// 用于GETALL和SETALL命令
    struct seminfo *__buf;// 用于IPC_INFO命令
};
struct seminfo
{
    int semmap;// Linux内核没有使用
    int semmni;// 系统最多可以拥有的信号量集数目
    int semmns;// 系统最多可以拥有的信号量数目
    int semmnu;// Linux内核没有使用
    int semmsl;// 一个信号量集最多允许包含的信号量数目
};

返回值:

  • semctl成功时的返回值取决于command参数,失败时返回 - 1,并设置errno

image-20241220111936954

注意事项

GETNCNTGETPIDGETVALGETZCNTSETVAL操作中,操作的是单个信号量,此时sem_num参数指定单个信号量在信号量集中的编号。而其他操作针对的是整个信号量集,此时sem_num参数被忽略。

5.特殊键值 IPC_PRIVATE

semget的调用者可以给其key参数传递一个特殊键值IPC_PRIVATE(其值为0),这样无论该信号量是否已存在,semget函数都将创建一个新信号量,使用该键值创建的信号量并非像它的名字声称的那样是进程私有的,其他进程,尤其是子进程,也有方法来访问这个信号量,所以semget函数的man手册的BUGS部分上说,使用名字IPC_PRIVATE有些误导(历史原因),应称为IPC_NEW

6.信号量实现进程间通信

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <string.h>
#include <unistd.h>

// 定义链表节点结构体
typedef struct ListNode {
    int data;
    struct ListNode *next;
} ListNode;

// 定义信号量操作结构体
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short int *array;
    struct seminfo *__buf;
};

// 声明全局的链表头指针
ListNode *head = NULL;  

// 信号量操作函数
void semaphore_op(int semid, int sem_num, int op) {
    struct sembuf sem_b;
    sem_b.sem_num = sem_num;
    sem_b.sem_op = op;
    sem_b.sem_flg = 0;
    if (semop(semid, &sem_b, 1) == -1) {
        perror("semop");
        exit(1);
    }
}

// 生产者函数
void producer(int semid) {
    ListNode *head = NULL;  // 链表头指针
    int item = 0;
    while (1) {
        // 申请节点内存
        ListNode *new_node = (ListNode *)malloc(sizeof(ListNode));
        if (new_node == NULL) {
            perror("malloc");
            exit(1);
        }
        new_node->data = item++;
		
         //P操作 获取空闲缓冲区信号量(这里表示链表插入的空位,第二参数表示产品还剩下0)
        semaphore_op(semid, 0, -1); 

        // 将新节点插入链表头部(简单实现,可按需改为其他插入方式)
        new_node->next = head;
        head = new_node;
		printf("生产了一个产品\n");
        // V操作 增加产品信号量(表示链表中有新数据可供消费,第二参数表示生产出来了一个产品)
        semaphore_op(semid, 1, 1);  

        sleep(1);
    }
}

// 消费者函数
void consumer(int semid) {
    while (1) {
        semaphore_op(semid, 1, -1);  // P操作 获取产品信号量(链表中有数据才可消费,第二个参数1代表有数据)
		
        // 取出链表头节点进行消费
        ListNode *node_to_consume = NULL;
        if (head!= NULL) {
            node_to_consume = head;
            head = head->next;
            printf("Consumer: %d\n", node_to_consume->data);
            free(node_to_consume);  // 释放消费完的节点内存
        }
		printf("消费了一个产品\n");
        semaphore_op(semid, 0, 1);  // V操作 增加空闲缓冲区信号量(链表腾出空位,第二个参数代表已经消费完了)

        sleep(2);
    }
}

int main() {
    // 创建信号量集  IPC_PRIVATE的值为0,表示不管该信号量创建了没有都会创建一个
    int semid = semget(IPC_PRIVATE, 2, 0666 | IPC_CREAT);
    if (semid == -1) {
        perror("semget");
        return 1;
    }
    union semun arg;
    arg.val = 1;
    // 初始化空闲缓冲区信号量(初始有1个空位可插入链表节点)
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl");
        return 1;
    }
    arg.val = 0;
    // 初始化产品信号量(初始链表无数据可供消费)
    if (semctl(semid, 1, SETVAL, arg) == -1) {
        perror("semctl");
        return 1;
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程为消费者
        consumer(semid);
    } else {
        // 父进程为生产者
        producer(semid);
    }

    // 删除信号量集
    if (semctl(semid, 0, IPC_RMID) == -1) {
        perror("semctl");
        return 1;
    }

    return 0;
}

总体功能:

生产者生产一个,消费者拿一个,然后再生产,然后再消费

1. 数据结构定义

定义了ListNode结构体来表示链表的节点,包含一个int类型的数据成员用于存放生产者生产的数据,以及一个指向下一个节点的指针成员。

2. 信号量操作相关部分
  • union semun结构体:用于给semctl函数传递参数,根据不同的命令可以传递不同类型的值,在这里主要用于初始化信号量的值。
  • semaphore_op函数:封装了semop函数,用于对信号量进行操作。它接受信号量集标识符、信号量编号以及操作值作为参数,构造struct sembuf结构体并调用semop函数来执行信号量操作,操作失败时会输出错误信息并终止程序。
3. 生产者逻辑
  • producer函数中,首先定义了链表头指针head,并在循环中不断生产数据。每次生产时,先通过malloc函数申请一个新的链表节点内存空间,将数据存入节点。
  • 然后通过semaphore_op函数获取空闲缓冲区信号量(这里代表链表中可插入新节点的空位),接着将新节点插入到链表头部(简单实现了链表插入操作,实际可根据需求调整插入逻辑),再通过semaphore_op函数增加产品信号量,表示链表中有新的数据可供消费者消费,最后通过sleep函数模拟生产过程的时间间隔。
4. 消费者逻辑
  • consumer函数中,通过循环不断尝试消费数据。首先通过semaphore_op函数获取产品信号量,只有当链表中有数据(信号量值大于等于 1)时才能继续执行。
  • 接着取出链表头节点,将其数据打印出来模拟消费过程,然后释放该节点占用的内存空间,最后通过semaphore_op函数增加空闲缓冲区信号量,表示链表腾出了一个空位可供生产者插入新节点,同样通过sleep函数模拟消费过程的时间间隔。
5. 主函数部分
  • main函数中,通过semget函数创建包含两个信号量的信号量集,分别用于控制空闲缓冲区(链表插入空位)和产品(链表中可消费的数据)。
  • 使用semctl函数结合union semun结构体来初始化这两个信号量的初始值。
  • 通过fork函数创建子进程,子进程执行consumer函数作为消费者,父进程执行producer函数作为生产者。
  • 最后在程序结束时,通过semctl函数删除信号量集,释放相关系统资源。

这样就通过 System V IPC 信号量实现了一个基于链表作为共享数据结构的生产者 - 消费者模型,确保了生产者和消费者对链表的并发访问是安全有序的。

运行结果:

image-20241220115519889

7.无血缘关系进程通信

  1. ftok函数的定义和功能
    • 函数原型key_t ftok(const char *pathname, int proj_id);
    • 功能ftok函数用于生成一个唯一的key(键值),这个key通常用于 System V IPC(进程间通信)机制中,如创建共享内存、消息队列和信号量集等。它将一个文件路径名(pathname)和一个项目标识符(proj_id)组合起来,生成一个适合作为 System V IPC 资源标识符的key值。
  2. 参数解释
    • const char *pathname
      • 这是一个指向文件路径名的指针。这个文件路径必须是一个已经存在的文件的有效路径,通常使用当前目录(.")或者一个程序相关的配置文件路径等。ftok函数会使用文件的inode(索引节点)信息作为生成key的一部分。
      • 注意,如果文件被删除然后重新创建,即使文件名相同,inode可能会改变,这会导致ftok生成不同的key值。
    • int proj_id
      • 这是一个0 - 255之间的整数,作为项目标识符。它和文件路径的inode信息一起组合生成key。不同的项目可以使用不同的proj_id来区分,这样即使基于同一个文件路径,不同的项目也能生成不同的key值用于各自的 IPC 资源。例如,一个程序中有两个不同的模块需要使用消息队列进行通信,它们可以使用相同的文件路径但不同的proj_id来生成不同的key,以创建两个独立的消息队列。
  3. 返回值
    • 成功时,ftok函数返回一个key_t类型的非负整数,这个整数可以作为shmgetmsggetsemget等 System V IPC 函数的key参数来创建或获取对应的 IPC 资源。
    • 失败时,返回-1,并且会设置errno来指示错误原因。常见的错误原因包括:
      • EACCESS:没有权限访问pathname指定的文件。
      • ENOENTpathname指定的文件不存在。

发送端

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

// 信号量操作函数
void semaphore_op(int semid, int sem_num, int op) {
    struct sembuf sem_b;
    sem_b.sem_num = sem_num;
    sem_b.sem_op = op;
    sem_b.sem_flg = 0;
    if (semop(semid, &sem_b, 1) == -1) {
        perror("semop");
        exit(1);
    }
}

int main() {
    // 通过ftok生成key
    key_t key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    // 创建信号量集(只含一个信号量)
    int semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        return 1;
    }

    // 初始化信号量值为0(表示资源不可用)
    union semun {
        int val;
        struct semid_ds *buf;
        unsigned short *array;
        struct seminfo *__buf;
    } arg;
    arg.val = 0;
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl");
        return 1;
    }

    // 进行一些操作后,释放信号量(表示资源可用了)
    semaphore_op(semid, 0, 1);
    printf("Sender process released the signal...\n");
	
    
    sleep(10);
    return 0;
}

读入端

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>

// 信号量操作函数
void semaphore_op(int semid, int sem_num, int op) {
    struct sembuf sem_b;
    sem_b.sem_num = sem_num;
    sem_b.sem_op = op;
    sem_b.sem_flg = 0;
    if (semop(semid, &sem_b, 1) == -1) {
        perror("semop");
        exit(1);
    }
}

int main() {
    // 通过ftok生成key
    key_t key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    // 获取信号量
    int semid = semget(key, 1, 0666);
    if (semid == -1) {
        perror("semget");
        return 1;
    }

    // 等待信号量,相当于等待另一个进程释放资源
    semaphore_op(semid, 0, -1);
    printf("Receiver process got the signal and can continue...\n");

    // 操作完成后,释放信号量(这里简单示意)
    semaphore_op(semid, 0, 1);

    // 删除信号量集
    if (semctl(semid, 0, IPC_RMID, NULL) == -1) {
        perror("semctl for delete");
    }
	sleep(10);
    return 0;
}

运行结果:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/939841.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深入理解Kafka:核心设计与实践原理读书笔记

目录 初识Kafka基本概念安装与配置ZooKeeper安装与配置Kafka的安装与配置 生产与消费服务端参数配置 生产者客户端开发消息对象 ProducerRecord必要的参数配置发送消息序列化分区器生产者拦截器 原理分析整体架构元数据的更新 重要的生产者参数acksmax.request.sizeretries和re…

electron 顶部的元素点不中,点击事件不生效

electron 顶部的元素点不中&#xff0c;点击事件不生效

Excel设置生日自动智能提醒,公式可直接套用!

大家好&#xff0c;我是小鱼。 今天跟大家分享一个WPS表格中根据出生日期&#xff0c;设置生日提醒&#xff0c;并且根据距离生日天数自动标记数据颜色。简单又实用&#xff0c;一个公式轻松搞定&#xff01; 接下来我们先学习一下需要使用到的函数&#xff0c;然后再根据实例让…

全域数据集成平台ETL

全域数据集成平台ETL Restcloud 工作原理 RestCloud数据集成平台采用SpringCloud微服务架构技术开发&#xff0c;底层基于纯Java语言采用前后端分离架构&#xff0c;前端采用React技术进行开发。 RestCloud数据集成平台是基于数据流工作流引擎的架构进行研发的&#xff0c;底…

Spring(一)---IOC(控制权反转)

目录 引入 1.什么叫IOC(Inversion of Control)控制权反转&#xff1f; 2.什么叫AOP(Aspect-Oriented Programming)面向切面编程(涉及Java代理)&#xff1f; 3.简单谈一下Java怎么实现ICO? Spring框架的介绍 1. Spring框架的概述 2. Spring框架的优点 Spring IOC容器介绍…

【GESP】C++二级考试大纲知识点梳理, (4)流程图

GESP C二级官方考试大纲中&#xff0c;共有9条考点&#xff0c;本文针对C&#xff08;4&#xff09;号知识点进行总结梳理。 &#xff08;4&#xff09;了解流程图的概念及基本表示符号&#xff0c;掌握绘制流程图的方法&#xff0c;能正确使用流程图描述程序设计的三种基本结构…

scala中正则表达式的使用

正则表达式&#xff1a; 基本概念 在 Scala 中&#xff0c;正则表达式是用于处理文本模式匹配的强大工具。它通过java.util.regex.Pattern和java.util.regex.Matcher这两个 Java 类来实现&#xff08;因为 Scala 运行在 Java 虚拟机上&#xff0c;可以无缝使用 Java 类库&…

使用VSCode Debugger 调试 React项目

一般我们调试代码时&#xff0c;用的最多的应该就是console.log方式了&#xff0c;还有的是使用Chrome DevTools 通过在对应的 sourcemap代码位置打断点进行调试&#xff0c;除了上面两种方式外还有一种更好用的调试方式&#xff1a; VSCode Debugger。 VSCode Debugger可以直…

微信小程序实现上传图片自定义水印功能、放大缩小旋转删除、自定义字号颜色位置、图片导出下载、图像预览裁剪、Canvas绘制 开箱即用

目录 功能实现画布绘制上传图片并渲染图片操作事件添加文字水印canvas解析微信小程序中 canvas 的应用场景canvas 与 2D 上下文、webgl 上下文的关系图像的加载与绘制总结说明功能实现 画布绘制 在wxml添加canvas标签并在在当前页面的 data 对象中,创建一个 Canvas 上下文(c…

用.Net Core框架创建一个Web API接口服务器

我们选择一个Web Api类型的项目创建一个解决方案为解决方案取一个名称我们这里选择的是。Net 8.0框架 注意&#xff0c;需要勾选的项。 我们找到appsetting.json配置文件 appsettings.json配置文件内容如下 {"Logging": {"LogLevel": {"Default&quo…

[创业之路-199]:《华为战略管理法-DSTE实战体系》- 3 - 价值转移理论与利润区理论

目录 一、价值转移理论 1.1. 什么是价值&#xff1f; 1.2. 什么价值创造 &#xff08;1&#xff09;、定义 &#xff08;2&#xff09;、影响价值创造的因素 &#xff08;3&#xff09;、价值创造的三个过程 &#xff08;4&#xff09;、价值创造的实践 &#xff08;5&…

【阅读记录-章节6】Build a Large Language Model (From Scratch)

文章目录 6. Fine-tuning for classification6.1 Different categories of fine-tuning6.2 Preparing the dataset第一步&#xff1a;下载并解压数据集第二步&#xff1a;检查类别标签分布第三步&#xff1a;创建平衡数据集第四步&#xff1a;数据集拆分 6.3 Creating data loa…

[搜广推]王树森推荐系统——矩阵补充最近邻查找

矩阵补充&#xff08;工业界不常用&#xff09; 模型结构 embedding可以把 用户ID 或者 物品ID 映射成向量输入用户ID 和 物品ID&#xff0c;输出向量的内积&#xff08;一个实数&#xff09;&#xff0c;内积越大说明用户对这个物品越感兴趣模型中的两个embedding层不共享参…

【优选算法篇】揭秘快速排序:分治算法如何突破性能瓶颈

文章目录 须知 &#x1f4ac; 欢迎讨论&#xff1a;如果你在学习过程中有任何问题或想法&#xff0c;欢迎在评论区留言&#xff0c;我们一起交流学习。你的支持是我继续创作的动力&#xff01; &#x1f44d; 点赞、收藏与分享&#xff1a;觉得这篇文章对你有帮助吗&#xff1…

建投数据与腾讯云数据库TDSQL完成产品兼容性互认证

近日&#xff0c;经与腾讯云联合测试&#xff0c;建投数据自主研发的人力资源信息管理系统V3.0、招聘管理系统V3.0、绩效管理系统V2.0、培训管理系统V3.0通过腾讯云数据库TDSQL的技术认证&#xff0c;符合腾讯企业标准的要求&#xff0c;产品兼容性良好&#xff0c;性能卓越。 …

Java-30 深入浅出 Spring - IoC 基础 启动IoC 纯XML启动 Bean、DI注入

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 大数据篇正在更新&#xff01;https://blog.csdn.net/w776341482/category_12713819.html 目前已经更新到了&#xff1a; MyBatis&#xff…

如何利用Python爬虫获得1688按关键字搜索商品

在当今的数字化时代&#xff0c;数据已成为企业竞争的核心资源。对于电商行业来说&#xff0c;了解市场动态、分析竞争对手、获取商品信息是至关重要的。Python作为一种强大的编程语言&#xff0c;其丰富的库和框架使得数据爬取变得简单易行。本文将介绍如何使用Python爬虫技术…

WatchAlert - 开源多数据源告警引擎

概述 在现代 IT 环境中&#xff0c;监控和告警是确保系统稳定性和可靠性的关键环节。然而&#xff0c;随着业务规模的扩大和数据源的多样化&#xff0c;传统的单一数据源告警系统已经无法满足复杂的需求。为了解决这一问题&#xff0c;我开发了一个开源的多数据源告警引擎——…

Leetcode中最常用的Java API——util包

前言&#xff1a;在刷力扣的时候是核心代码模式&#xff0c;笔试的时候很可能是ACM模式&#xff0c;需要自己完成导包、定义和自行设计输出&#xff0c;所以一些常用的类和方法需要先导入相应的API包&#xff0c;java.util就是最常用到的包&#xff0c;因为它包含集合这个大框架…

基于文件流的图书管理系统(C/C++实现)

基于文件流的图书管理系统&#xff08;C/C实现&#xff09; 一、项目背景 在日常的图书馆管理中&#xff0c;图书的管理往往需要涉及到对图书数据的增删查改&#xff08;CRUD&#xff09;操作。为了更好地管理图书信息&#xff0c;我们可以利用C的文件流&#xff08;fstream&a…