C++ TinyWebServer项目总结(13. 多进程编程)

news2024/9/24 23:28:10

本章讨论Linux多进程编程的以下内容:

  • 复制进程映像的fork系统调用和替换进程映像的exec系列系统调用。
  • 僵尸进程以及如何避免僵尸进程。
  • 进程间通信(Inter Process Communication,IPC)最简单的方式:管道。
  • 三种System V进程间通信方式:信号量、消息队列、共享内存。它们是由AT&T System V2版本的UNIX引入的,所以统称为System V IPC。
  • 在进程间传递文件描述符的通用方法:通过UNIX本地域socket传递特殊的辅助数据。(关于辅助数据,参考《Linux 高性能服务器编程》P85)

fork 系统调用

Linux下创建新进程的系统调用是fork

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

该函数的每次调用都返回两次,在父进程中返回的是子进程的PID,在子进程中则返回0,该返回值是后续代码判断当前进程是父进程还是子进程的依据。fork系统调用失败时返回-1,并设置errno。

fork函数复制当前进程,在内核进程表中创建一个新的进程表项,新的进程表项中很多属性和原进程相同,如堆指针、栈指针、标志寄存器的值,但也有很多属性被赋予了新值,如子进程的PPID被设置成原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。

子进程的代码和父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据、静态数据),数据的复制采用的是写时复制(copy on write),即只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据),即便如此,如果我们在程序中分配了大量内存,那么使用fork函数时也应当谨慎,尽量避免没必要的内存分配和数据复制。

此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。父进程的用户根目录、当前工作目录等变量的引用计数也会加 1。

exec 系列系统调用

有时我们需要在子进程中执行其他程序,即替换当前进程映像,这就需要以下exec系列函数之一:

#incldue <unistd.h>
extern char** environ;

int execl(const char* path, const char* arg, ...);
int execlp(const char* file, const char* arg, ...);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execv(const char* path, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execve(const char* path, char* const arg[], char* const envp[]);
参数

path:指定可执行文件的完整路径;

file:文件名,该文件的具体位置在环境变量PATH中搜寻。

arg:接受可变参数,

argv:则接受参数数组,它和 arg 都会被传递给新程序(pathfile参数指定的程序)的main函数。

envp:用于设置新程序的环境变量,如果未设置它,则新程序将使用由全局变量environ指定的环境变量。

返回值

一般,exec函数是不返回的,除非出错,此时它返回-1,并设置errno。如果没出错,则原进程中exec调用之后的代码都不会执行,因为此时原程序已经被exec的参数指定的程序完全替换(包括代码和数据)。

exec函数不会关闭原进程打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC的属性。

SOCK_CLOEXEC属性用于在子进程中关闭 socket,见《Linux 高性能服务器编程》P75

处理僵尸进程

对多进程程序而言,父进程一般需要跟踪子进程的退出状态,因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)。

子进程进入僵尸态的两种情况:

  • 在子进程结束运行后,父进程读取其退出状态前,我们称该子进程处于僵尸态。
  • 父进程结束或者异常终止,而子进程继续运行时,此时子进程的PPID将被操作系统设置为1,即init进程,init进程接管了该子进程,并等待它结束。在父进程退出之后,子进程退出之前,该子进程也处于僵尸态。

如果父进程没有正确处理子进程的返回信息,子进程将停留在僵尸态,并占据着内核资源,这是不能容许的,因为内核资源有限,以下函数在父进程中调用,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或使子进程从僵尸态结束:

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int option);

wait函数将阻塞进程,直到该进程的某个子进程结束运行,它返回结束运行的子进程的PID,并将该子进程的退出状态信息存储于stat_loc参数指向的内存中,sys/wait.h文件中定义了以下宏来帮助解释子进程的退出状态信息:

上图中有一个错误,WIFSTOPPED宏是用来判断子进程是否是被信号暂停。

wait函数的阻塞特性不是服务器程序期望的,而waitpid函数解决了这个问题。waitpid函数只等待由pid参数指定的子进程,如果pid参数取值为-1,那么它就和wait函数相同,即等待任意一个子进程结束。waitpid函数的stat_loc参数的含义和wait函数的stat_loc参数的相同。options参数可以控制waitpid函数的行为,该参数最常用的取值为WNOHANG,此时waitpid函数是非阻塞的,如果pid指定的目标子进程尚未终止,则waitpid函数立即返回0,如果目标子进程确实正常退出了,则waitpid函数返回该子进程的PID。waitpid函数失败时返回-1并设置errno。

要在事件已经发生的情况下执行非阻塞调用才能提高程序的效率。对于 waitpid 函数,我们最好在某个子进程退出之后再调用它。

当一个进程结束时,它将给父进程发送一个SIGCHLD信号,我们可以在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以“彻底结束”一个子进程:

static void handle_child(int sig) {
    pid_t pid;
    int stat;
    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
        // 对结束的子进程进行善后处理
    }
}

管道

管道可以实现进程内部的通信。pipe 函数

管道也是父进程和子进程间通信的常用手段。

管道能在父、子进程间传递数据,利用的是调用fork后两个管道文件描述符都保持打开,一对这样的文件描述符能保证父子进程间一个方向的数据传输,父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1]

如果要实现父子进程之间的双向数据传输,可以使用两个管道。socket编程接口提供了一个创建全双工管道的系统调用socketpair

管道只能用于有关联的两个进程(如父、子进程)间的通信,而以下要讨论的三种System V IPC 能用于无关联的多个进程之间的通信,因为它们都使用一个全局唯一的键值来标识一条信道。有一种特殊的管道称为FIFO(First In First Out,先进先出),也叫命名管道,它也能用于无关联进程之间的通信,但FIFO管道在网络编程中用得不多,所以我们不讨论它。

信号量

信号量原语

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

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两个值,我们仅讨论二进制信号量。使用二进制信号量同步两个进程,以确保关键代码段的独占式访问的例子:

上图中,当关键代码段可用时,二进制信号量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。它们被设计为操作一组信号量,即信号量集,而不是单个信号量。

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;					/* 访问权限 */
                                	/* 省略其他填充字段 */
};

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的影响。
  • sem_flg:可选值为IPC_NOWAITSEM_UNDO

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

返回值

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

semctl 系统调用

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

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

特殊键值 IPC_PRIVATE

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

共享内存

共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输,这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件,因此,共享内存通常和其他进程间通信方式一起使用。

Linux共享内存的API都定义在sys/shm.h头文件中,包括4个系统调用shmgetshmatshmdtshmctl

shmget 系统调用

shmget 系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。

#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

shmget 成功时返回一个正整数值,它是共享内存的标识符;失败时返回-1 并设置 errno。

shmatshmdt 系统调用

共享内存被创建/获取后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中,使用完共享内存后,我们也需要将它从进程地址空间中分离,这两项任务分别由以下两个系统调用实现:

#include <sys/shm.h>
void* shmat(int shm_id, const void* shm_addr, int shmflg);
int shmdt(const void* shm_addr);

shm_id参数是由shmget函数返回的共享内存标识符。shm_addr参数指定将共享内存关联到进程的哪块地址空间,最终效果还受到shmflg参数的可选标志SHM_RND的影响。

shmat函数成功时返回共享内存被关联到的地址,失败则返回(void *)-1并设置errno。

shmdt函数将关联到shm_addr参数处的共享内存从进程中分离,它成功时返回0,失败则返回-1并设置errno。

shmctl 系统调用

shmctl系统调用控制共享内存的某些属性。

#include <sys/shm.h>
int shmctl(int shm_id, int command, struct shmid_ds* buf);

共享内存的 POSIX 方法

mmap函数和munmap函数 利用mmap函数的MAP_ANONYMOUS标志可以实现父、子进程间的匿名内存共享。通过打开同一个文件,mmap也可以实现无关进程之间的内存共享。

Linux提供了另一种在无关进程间共享内存的方式,这种方式无须任何文件的支持,但它需要先用shm_open函数来创建或打开一个POSIX共享内存对象:

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

int shm_open ( const char* name, int oflag, mode_t mode );

shm_open函数的使用方法与open系统调用完全相同。

shm_open函数成功时返回一个文件描述符,该文件描述符可用于后续的mmap调用,从而将共享内存关联到调用进程。shm_open函数失败时返回-1,并设置errno。

和打开的文件最后需要关闭一样,由shm_open函数创建的共享内存对象用完后也需要删除,可通过shm_unlink函数实现:

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

int shm_unlink ( const char* name );

shm_unlink函数将name参数指定的共享内存对象标记为等待删除,当所有使用该共享内存对象的进程都使用munmap函数将它从进程中分离后,系统将销毁这个共享内存对象所占据的资源。

如果代码中使用了以上POSIX共享内存函数,则编译时需要指定链接选项-lrt

共享内存实例

暂略。

消息队列

消息队列是在两个进程间传递二进制数据块的方式,每个数据块都有一个特定类型,接收方可以根据类型来有选择地接收数据,而不一定像管道和命名管道那样必须以先进先出的方式接收数据。

Linux消息队列的API都定义在sys/msg.h头文件中,包括4个系统调用:msggetmsgsndmsgrcvmsgctl

msgget 系统调用

msgget系统调用创建一个消息队列,或获取一个已有的消息队列:

#include <sys/msg.h>
int msgget(key_t key, int msgflg);

key参数是一个键值,用来标识一个全局唯一的消息队列。

msgget函数成功时返回一个正整数值,它是消息队列的标识符,失败时返回-1并设置errno。

msgsnd 系统调用

msgsnd系统调用将一条消息添加到消息队列中:

#include <sys/msg.h>
int msgsnd(int msqid, const void* msg_ptr, size_t msg_sz, int msgflg);

struct msgbuf{
	long mtype;//消息类型
	char mtext[512];//消息数据
};

msqid参数是由msgget函数返回的消息队列标识符。

msg_ptr参数指向一个准备发送的消息,消息被定义为如下类型:

struct msgbuf{
	long mtype;			/* 消息类型 */
	char mtext[512];	/* 消息数据 */
};

msgrcv 系统调用

msgrcv系统调用从消息队列中获取消息:

#include <sys/msg.h>
int msgrcv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);

msgctl 系统调用

msgctl系统调用控制消息队列的某些属性:

#incldue <sys/msg.h>
int msgctl(int msqid, int command, struct msqid_ds* buf);

IPC 命令

以上3种System V IPC进程间通信方式都使用一个全局唯一的键值来描述一个共享资源,当程序调用semgetshmgetmsgget时,就创建了这些共享资源的一个实例。Linux提供ipcs命令来观察当前系统上拥有哪些共享资源实例:

输出结果分段显示了系统拥有的消息队列、共享内存、信号量资源,可见,该系统目前尚未使用任何消息队列和信号量,但分配了一组键值为0的共享内存。

可用ipcrm命令删除遗留在系统中的共享资源。

实战 10: 在进程间传递文件描述符

fork调用后,父进程中打开的文件描述符在子进程中仍然保持打开,所以文件描述符可以很方便地从父进程传递到子进程。注意,传递一个文件描述符并不是传递一个文件描述符的值,而是在接收进程中创建一个新的文件描述符,且新文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项。

要想在两个不相干的进程之间传递文件描述符,在Linux下,可利用UNIX域socket在进程间传递特殊的辅助数据,以实现文件描述符的传递,下例代码中,子进程中打开一个文件描述符,然后将它传递给父进程,父进程则通过读取该文件描述符来获得文件内容:

#include <sys/socket.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

static const int CONTROL_LEN = CMSG_LEN(sizeof(int));
// 发送文件描述符,fd参数是用来传递信息的UNIX域socket,fd_to_send参数是待发送的文件描述符
void send_fd(int fd, int fd_to_send) {
    struct iovec iov[1];
    struct msghdr msg;
    char buf[0];

    iov[0].iov_base = buf;
    iov[0].iov_len = 1;
    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    cmsghdr cm;
    cm.cmsg_len = CONTROL_LEN;
    cm.cmsg_level = SOL_SOCKET;
    cm.cmsg_type = SCM_RIGHTS;
    *(int *)CMSG_DATA(&cm) = fd_to_send;
    msg.msg_control = &cm;		/* 设置辅助数据 */
    msg.msg_controllen = CONTROL_LEN;

    sendmsg(fd, &msg, 0);       /* 通用数据读 */
}

// 接收目标文件描述符
int recv_fd(int fd) {
    struct iovec iov[1];
    struct msghdr msg;
    char buf[0];

    iov[0].iov_base = buf;
    iov[0].iov_len = 1;
    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    cmsghdr cm;
    msg.msg_control = &cm;
    msg.msg_controllen = CONTROL_LEN;

    recvmsg(fd, &msg, 0);       /* 通用数据写 */

    int fd_to_read = *(int *)CMSG_DATA(&cm);
    return fd_to_read;
}

int main() {
    int pipefd[2];
    int fd_to_pass = 0;
    /* 创建父、子进程间的管道,文件描述符pipefd[0]和pipefd[1]都是UNIX域socket */
    int ret = socketpair(PF_UNIX, SOCK_DGRAM, 0, pipefd);
    assert(ret != -1);

    pid_t pid = fork(); /* 创建子进程 */
    assert(pid >= 0);

    if (pid == 0) {
        close(pipefd[0]);                                       /* 子进程关闭读 */
        fd_to_pass = open("test.txt", O_RDWR, 0666);            /* 打开文件 */
        send_fd(pipefd[1], (fd_to_pass > 0) ? fd_to_pass : 0);  /* 子进程通过管道将文件描述符发送到父进程,如果文件打开失败,则子进程将标准输入发送到父进程 */
        close(fd_to_pass);
        exit(0);
    }

    close(pipefd[1]); /* 父进程关闭写 */

    fd_to_pass = recv_fd(pipefd[0]);    /* 父进程从管道接收目标文件描述符 */
    char buf[1024];                     /* 存放数据 */
    memset(buf, '\0', 1024);
    // 读目标文件描述符,验证其有效性
    read(fd_to_pass, buf, 1024);
    printf("I got fd %d and data %s\n", fd_to_pass, buf);
    close(fd_to_pass);
}
123

结构体 iovec见:readv函数和writev函数

效果:

参考文章

  1. Linux高性能服务器编程-游双——第十三章 多进程编程_linux高性能服务器编程 pdf-CSDN博客
  2. Linux高性能服务器编程 学习笔记 第十三章 多进程编程_squid和socketpair-CSDN博客

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浏览器插件利器--allWebPluginV2.0.0.18-alpha版发布

allWebPlugin简介 allWebPlugin中间件是一款为用户提供安全、可靠、便捷的浏览器插件服务的中间件产品&#xff0c;致力于将浏览器插件重新应用到所有浏览器。它将现有ActiveX控件直接嵌入浏览器&#xff0c;实现插件加载、界面显示、接口调用、事件回调等。支持Chrome、Firefo…

[MRCTF2020]Unravel!!

使用zsteg查看图片有隐藏文件&#xff0c;没有头绪&#xff0c;先放弃 使用zsteg和010editor查看都发现一个png图片 把JM.png拷贝到kali&#xff0c;使用binwalk分离&#xff0c;得到一个aes.png 使用010editor查看wav&#xff0c;发现尾部有可疑的字符串&#xff0c;拷贝出来备…

记一次应急响应之网站暗链排查

目录 前言 1. 从暗链而起的开端 1.1 暗链的介绍 1.2 暗链的分类 2. 在没有日志的情况下如何分析入侵 2.1 寻找指纹 2.2 搜索引擎搜索fofa资产搜索 2.2.1 fofa资产搜索 2.2.2 bing搜索引擎搜索 3.通过搭建系统并进行漏洞复现 4. 应急响应报告编写 前言 免责声明 博文…

二叉树(binary tree)遍历详解

一、简介 二叉树常见的遍历方式包括前序遍历、中序遍历、后序遍历和层序遍历等。我将以下述二叉树来讲解这几种遍历算法。 1、创建二叉树代码实现 class TreeNode:def __init__(self,data):self.datadataself.leftNoneself.rightNonedef createTree():treeRootTreeNode(F)N…

大模型提示词工程技术3-提示词输入与输出的优化的技巧详细介绍

大模型提示词工程技术3-提示词输入与输出的优化的技巧详细介绍。《大模型提示词工程技术》的作者&#xff1a;微学AI&#xff0c;这是一本专注于提升人工智能大模型性能的著作&#xff0c;它深入浅出地讲解了如何通过优化输入提示词来引导大模型生成高质量、准确的输出。书中不…

腾讯地图三维模型加载GLTF,播放模型动画

腾讯地图三维模型加载&#xff0c;播放模型动画 关键代码 const clock new THREE.Clock();console.log(gltf)// 确保gltf对象包含scene和animations属性if (gltf && gltf.scene && gltf.animations) {// 创建AnimationMixer实例&#xff0c;传入模型的scenec…

【51单片机】2-3-1 【I/O口】【电动车防盗报警项目】震动传感器实验1—震动点灯

1.硬件 51单片机最小系统LED灯模块震动传感器模块 2.软件 main.c程序 #include "reg52.h"sbit led1 P3^7;//根据原理图&#xff08;电路图&#xff09;&#xff0c;设备变量led1指向P3组IO口的第7口 sbit vibrate P3^3;//Do接到了P3.3口void Delay2000ms() //…

力扣刷题--2185. 统计包含给定前缀的字符串【简单】

题目描述 给你一个字符串数组 words 和一个字符串 pref 。 返回 words 中以 pref 作为 前缀 的字符串的数目。 字符串 s 的 前缀 就是 s 的任一前导连续字符串。 示例 1&#xff1a; 输入&#xff1a;words [“pay”,“attention”,“practice”,“attend”], pref “at…

用 Higress AI 网关降低 AI 调用成本 - 阿里云天池云原生编程挑战赛参赛攻略

作者介绍&#xff1a;杨贝宁&#xff0c;爱丁堡大学博士在读&#xff0c;研究方向为向量数据库 《Higress AI 网关挑战赛》正在火热进行中&#xff0c;Higress 社区邀请了目前位于排行榜 top5 的选手杨贝宁同学分享他的心得。下面是他整理的参赛攻略&#xff1a; 背景 我们…

Jmeter(十四)Jmeter分布式部署测试

单个接口测试&#xff0c;我们使用谷歌的插件postman 多个接口测试&#xff0c;我们使用Jmeter进行测试 一、使用工具测试 1、使用Jmeter对接口测试 首先我们说一下为什么用Posman测试后我们还要用Jmeter做接口测试&#xff0c;在用posman测试时候会发现的是一个接口一个接…

存储实验:基于华为存储实现存储双活(HyperMetro特性)

目录 什么是存储双活仲裁机制 实验需求实验拓扑实验环境实验步骤1. 双活存储存储初始化&#xff08;OceanStor v3 模拟器&#xff09;1.1开机&#xff0c;设置密码1.2登录DM&#xff0c;修改设备名、系统时间和导入License1.3 设置接口IP 2. 仲裁服务器配置&#xff08;Centos7…

C++ 两线交点程序(Program for Point of Intersection of Two Lines)

示例图 给定对应于线 AB 的点 A 和 B 以及对应于线 PQ 的点 P 和 Q&#xff0c;找到这些线的交点。这些点在 2D 平面中给出&#xff0c;并带有其 X 和 Y 坐标。示例&#xff1a; 输入&#xff1a;A (1, 1), B (4, 4) C (1, 8), D (2, 4) 输出&#xff1a;给定直…

关于vue2运行时filemanager-webpack-plugin报错isFile is undefind

当我们在运行时报此错误时&#xff0c;在vue.config.js里找一下filemanager-webpack-plugin的配置路径。 new FileManagerPlugin({onEnd: {delete: [./dist.zip],archive: [{ source: ./dist, destination: ./dist.zip }]}}) 在对应的路径下建一个dist文件夹

scrapy--子类CrawlSpider中间件

免责声明:本文仅做分享参考~ 目录 CrawlSpider 介绍 xj.py 中间件 部分middlewares.py wyxw.py 完整的middlewares.py CrawlSpider 介绍 CrawlSpider类&#xff1a;定义了一些规则来做数据爬取&#xff0c;从爬取的网页中获取链接并进行继续爬取. 创建方式&#xff…

七年老玩家《王者荣耀》分析一:【市场与用户以及社交功能】

目录 市场与用户 王者荣耀在不同国家和地区的市场渗透率 王者荣耀的主要收入来源以及增长趋势 王者荣耀的用户活跃度和玩家留存率在最近几年的变化情况 王者荣耀面临的主要竞争对手以及如何在竞争中保持领先地位 《英雄联盟手游》&#xff08;LOL&#xff09; 《虚荣》&a…

手动安装Git,手动在右击菜单注册git运行程序

当我们有git的zip压缩包后&#xff0c;只将压缩包解压也是可以用的&#xff0c;但是每次使用时还得去git的安装包下启动git项目&#xff0c;这样就很麻烦。一般情况下都是右击就有git运行程序的选项&#xff0c;直接点击就好&#xff0c;这时用.exe文件安装就没问题&#xff0c…

智能报警物联网系统:使用MQTT和与Grafana集成的InfluxDB监控工地电梯流量和气象数据

这篇论文的标题是《Smart Alarm IoT System: Monitoring Elevator Traffic and Meteorological Data on Job Sites Using MQTT and InfluxDB integrated with Grafana》&#xff0c;作者们来自约旦大学的计算机工程系和机电工程系。以下是对论文主要内容的详细整理&#xff1a;…

LabVIEW波形图的多点触控实现方法

在LabVIEW中&#xff0c;如何实现波形图的多点触控功能&#xff0c;例如通过触控操作对波形进行放大和缩小&#xff1f; 解答&#xff1a; 在LabVIEW中&#xff0c;尽管原生支持的多点触控功能较为有限&#xff0c;但仍有多种方法可以实现波形图的触控操作、放大和缩小功能&am…

详解Asp.Net Core管道模型中的五种过滤器的适用场景与用法

1. 前言 在 ASP.NET Core 中&#xff0c;过滤器是一种用于对请求管道进行前置或后置处理的组件。它们可以在请求处理的不同阶段干预和修改请求和响应&#xff0c;以实现一些通用的处理逻辑或功能增强。 ASP.NET Core 的管道模型由多个中间件组成&#xff0c;而过滤器是这个模…

质量技术AI提效专题分享-得物技术沙龙

活动介绍 本次“质量技术&AI提效专题分享”沙龙聚焦于质量技术和AI效率领域&#xff0c;将为您带来四个令人期待的演讲话题&#xff1a; 1、《智能化提效实践》 2、《仿真自动化在饿了么金融实践分享》 3、《得物精准测试提效应用》 4、《广告算法灰度拦截实践》 相信这些…