(自用)多进程与信号

程序和进程

程序≠进程

产生进程

创建进程——fork函数

函数原型

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

函数功能:

fork函数的功能是创建一个与当前进程几乎完全相同的子进程。这个“几乎完全相同”指的是子进程会复制父进程的代码段、数据段、BSS段、堆、栈等所有用户空间信息,但它们在内核中的进程控制块(PCB)是不同的,因此拥有不同的进程ID。

返回值

fork函数的一个独特之处在于它“调用一次,返回两次”:

  • 在父进程中,fork函数返回新创建的子进程的进程ID。
  • 在子进程中,fork函数返回0。
  • 如果fork函数调用失败,则返回-1,并设置相应的errno值以指示错误原因(如EAGAIN表示达到进程数上限,ENOMEM表示系统内存不足)。

代码示例:

fork_test.c

#include <unistd.h>
#include <stdio.h>
#include<sys/types.h>
int main()
{
    pid_t fpid;//fpid表示fork函数返回的值
    int count = 0;
    fpid = fork();
    if (fpid < 0)
        printf("error in fork!");
    else if (fpid == 0) {//子进程
        printf("i am the child process, my process id is %d\n", getpid());
        printf("I’m children\n");
        count += 2;
    }
    else {//父进程
        printf("i am the parent process, my process id is %d/n", getpid());
        printf("I’m parent.\n");
        count++;
    }
    printf("统计结果是: %d/n", count);
    return 0;
}

分析:

最后父子进程都输出count的值

子进程输出2 父进程输出1

因为子进程、父进程的count不是同一份,子进程复制了父进程的count

注意:子进程、父进程getpid()输出的都是自己的进程id(子进程执行getpid()返回的不是0)

操作系统优化

子进程复制父进程所拥有的所有资源,这种方法使得创建进程非常非常非常慢,因为子进程需要拷贝父进程的所有的地址空间,那现代的操作系统,是如何处理的呢?主要有以下三种方式:

  • 写时复制

以fork_test.c为例,只有在父子进程对于count进行修改的时候,才对count进行复制。

  • 轻量级进程允许父子进程共享每进程在内核的很多数据结构,比如地址空间、打开文件表和信号处理。
  • vfork系统调用创建的进程能共享其父进程的内存地址空间,为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出为止。

进程销毁——exit函数

函数原型
 #include <stdlib.h>
 void exit(int status);
函数功能

进程调用exit函数时,进程结束,并返回状态码(保存在status中)

代码示例
 fpid = fork();
 if (fpid < 0)
     printf("error in fork!");
 else if (fpid == 0) {//子进程
     printf("i am the child process, my process id is %d\n", getpid());
     printf("I’m children\n");
     count += 2;
     exit(2);
 }

子进程以状态码2退出

父进程等待子进程结束——wait函数

函数原型
#include<sys/wait.h>
pid_t wait(int* _NULL wstatus)
函数功能

父进程调用wait函数来等待子进程结束

注意:1.wstatus地址所保存的值不是子进程退出的状态码,要用宏函数WEXITSTATUS

2.且wstatus可以为NULL

代码示例
#include <unistd.h>
#include <stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
    pid_t fpid;//fpid表示fork函数返回的值
    int count = 0;
    int status = 0;
    fpid = fork();
    if (fpid < 0)
        printf("error in fork!");
    else if (fpid == 0) {//子进程
        printf("i am the child process, my process id is %d\n", getpid());
        printf("I’m children\n");
        count += 2;
        exit(2);
    }
    else {//父进程
        printf("i am the parent process, my process id is %d/n", getpid());
        printf("I’m parent.\n");
        count++;
    }
    printf("统计结果是: %d/n", count);
    wait(&status);
    printf("parent : status : %d \n",WEXITSTATUS(status));
    return 0;
}

多进程高并发设计

不同的work进程在不同的核上运行

示例代码

下列代码创建了2个工作进程并绑定在CPU的不同核上工作,它们的工作内容为每隔10s输出“pid %ld ,doing ...\n", (long int)getpid()

fork_work_test.c

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include<sys/wait.h>
#include<errno.h>
#include<string.h>


typedef void (*spawn_proc_pt) (void* data);
static void worker_process_cycle(void* data);
static void start_worker_processes(int n);
pid_t spawn_process(spawn_proc_pt proc, void* data, char* name);

int main(int argc, char** argv) {
    start_worker_processes(2);
    //管理子进程
    wait(NULL);
}

void start_worker_processes(int n) {
    int i = 0;
    for (i = n - 1; i >= 0; i--) {
        spawn_process(worker_process_cycle, (void*)(intptr_t)i, "worker process");
    }
}

pid_t spawn_process(spawn_proc_pt proc, void* data, char* name) {

    pid_t pid;
    pid = fork();

    switch (pid) {
    case -1:
        fprintf(stderr, "fork() failed while spawning \"%s\"\n", name);
        return -1;
    case 0:
        proc(data);
        return 0;
    default:
        break;
    }
    printf("start %s %ld\n", name, (long int)pid);
    return pid;
}


static void worker_process_init(int worker) {
    cpu_set_t cpu_affinity;
    //worker = 2;
    //多核高并发处理  4core  0 - 0 core 1 - 1  2 -2 3 -3  
    CPU_ZERO(&cpu_affinity);
    CPU_SET(worker % CPU_SETSIZE, &cpu_affinity);// 0 1 2 3 
    //sched_setaffinity
    if (sched_setaffinity(0, sizeof(cpu_set_t), &cpu_affinity) == -1) {
        fprintf(stderr, "sched_setaffinity() failed,error desc:%s\n",strerror(errno));
    }
}

void worker_process_cycle(void* data) {
    int worker = (intptr_t)data;
    //初始化
    worker_process_init(worker);

    //干活
    for (;;) {
        sleep(10);
        printf("pid %ld ,doing ...\n", (long int)getpid());
    }
}

解析:

pid_t spawn_process(spawn_proc_pt proc, void* data, char* name) {

    pid_t pid;
    pid = fork();

    switch (pid) {
    case -1:
        fprintf(stderr, "fork() failed while spawning \"%s\"\n", name);
        return -1;
    case 0:
        proc(data);
        return 0;
    default:
        break;
    }
    printf("start %s %ld\n", name, (long int)pid);
    return pid;
}

proc表示要进程执行的函数名、data表示proc所需要的参数、name表示进程名

该函数会创建一个子进程去完成proc任务,父进程打印信息,eg:start work process 5436

void worker_process_cycle(void* data) {
    int worker = (intptr_t)data;
    //初始化
    worker_process_init(worker);

    //干活
    for (;;) {
        sleep(10);
        printf("pid %ld ,doing ...\n", (long int)getpid());
    }
}

在本例中,worker_process_cycle为表示分配给每个work_process的任务,它先进行初始化(主要是让每个进程关联对应的核),然后执行输出任务

static void worker_process_init(int worker) {
    cpu_set_t cpu_affinity;
    //worker = 2;
    //多核高并发处理  4core  0 - 0 core 1 - 1  2 -2 3 -3  
    CPU_ZERO(&cpu_affinity);
    CPU_SET(worker % CPU_SETSIZE, &cpu_affinity);// 0 1 2 3 
    //sched_setaffinity
    if (sched_setaffinity(0, sizeof(cpu_set_t), &cpu_affinity) == -1) {
        fprintf(stderr, "sched_setaffinity() failed,error desc:%s\n",strerror(errno));
    }
}

该函数用于设置进程在CPU的哪个核上工作

cpu_set_t

cpu_set_t 本质上是一个位集合(bit set),每一位代表一个CPU核心。如果一个位的值为1,表示该位对应的CPU核心包含在集合中;如果为0,则表示该CPU核心不在集合中。通过这种方式,cpu_set_t 能够表示一个或多个CPU核心的集合。

相关函数:

  • sched_setaffinity():该函数用于设置指定进程(或线程)的CPU亲和性,即指定该进程(或线程)应该在哪一个或哪些CPU核心上运行。
  • sched_getaffinity():与sched_setaffinity()相对应,这个函数用于获取指定进程(或线程)当前的CPU亲和性设置。
  • CPU_ZERO():用于初始化cpu_set_t变量,将所有位清零,即表示不包含任何CPU核心。
  • CPU_SET():用于将指定的CPU核心添加到cpu_set_t变量表示的集合中。
  • CPU_CLR():与CPU_SET()相反,用于从cpu_set_t变量表示的集合中移除指定的CPU核心。
  • CPU_ISSET():用于检查指定的CPU核心是否包含在cpu_set_t变量表示的集合中。

查看进程在cpu的核上执行

ps -eLo ruser,pid,lwp,psr,args | grep 可执行文件名

上图画圈部分表示进程运行的核的编号

僵尸_孤儿_守护进程(面试可能会问的概念)

  • 孤儿进程:
  • 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。

想想我们如何模仿一个孤儿进程?  答案是:  kill 父进程! 

  • 僵尸进程:
  •   一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种子进程称之为僵尸进程。
概括

子进程完成任务,但父进程没有执行wait或者waitpid,则子进程变为僵尸进程

特点

占用进程表项但不占用其他资源,可能会导致进程表溢出,从而影响新进程的创建。

PS:父进程提前结束,那么子进程的父进程会变为init,子进程结束后init进程会给子进程收尸

如果父进程陷入循环且没有调用wait或者waitpid,则子进程结束后,无人为其收尸,它会持续僵尸进程的状态.

查看僵尸进程

利用命令ps,可以看到有标记为<defunct>的进程就是僵尸进程。

守护进程
特点

1.不与任何终端关联

2.以特殊用户运行 (例如root)

3.处理一些系统级任务

成为守护进程的步骤

1.调用fork(),创建新进程,它会是将来的守护进程.

2.在父进程中调用exit,保证子进程不是进程组长

3.调用setsid()创建新的会话区

4.将当前目录改成目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录)

5.将标准输入,标输出,标准错误重定向到/dev/null.

使进程变为守护进程的代码:

#include <fcntl.h>
#include <unistd.h>

int daemon(int nochdir, int noclose)
{
    int fd;

    switch (fork()) {
    case -1:
        return (-1);
    case 0:
        break;
    default:
        _exit(0);//父进程直接退出
    }

    if (setsid() == -1)//创建新的会话区
        return (-1);

    if (!nochdir)//将当前目录改成根目录
        (void)chdir("/");//chdir将当前目录改为根目录

    if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) {
        (void)dup2(fd, STDIN_FILENO);
        (void)dup2(fd, STDOUT_FILENO);
        (void)dup2(fd, STDERR_FILENO);
        if (fd > 2)
            (void)close(fd);
    }//将标准输入,标准输出,标准错误重定向到/dev/null.
    return (0);
}

进程间通信

信号

不能自定义,所有信号都是系统预定义的。

注意:SIGKILL和SIGSTOP不能被捕获,即,这两种信号的响应动作不能被改变

信号的捕获——signal函数、sigaction函数

signal函数
函数原型
#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);

handler可以设为三种方式:

1.SIG_IGN 对signum对应的信号进行忽略
2.SIG_DFL对signum对应的信号进行默认处理

3.函数名,接收到signum对应的信号时,执行对应函数且函数的返回值一定要为void,参数一定要为int

返回值

signal的返回类型和第二个参数的类型都是函数指针。

sigaction函数(项目实战强烈推荐使用)

 sigaction与signal的区别: sigaction比signal更“健壮”,建议使用sigaction

函数原型
#include<signal.h>
int sigaction(int signum,
                     const struct sigaction *_Nullable restrict act,
                     struct sigaction *_Nullable restrict oldact);

关于信号处理的相关配置都放在struct sigaction结构体中,

act表示新的配置,oldact用来储存旧的配置。

sigaction结构体
struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };

(*sa_handler)(int): 信号处理函数

sa_mask:信号屏蔽集,在执行信号处理函数时,暂时阻塞该集合内的信号(等到信号处理函数执行结束后再响应)。

信号集相关的函数:

a.sigemptyset():用于初始化一个空的信号集,即将所有信号设置为未阻塞状态。

用法示例:

#include <signal.h>

sigset_t set;
sigemptyset(&set);

b.sigfillset(sigset_t *set):该函数用于将所有信号添加到信号集set中,即将所有信号设置为阻塞状态

用法示例:

#include <signal.h>

sigset_t set;
sigfillset(&set);

c.sigaddset(sigset_t *set, int signum):该函数用于将指定信号signum添加到信号集set中,将指定信号设置为阻塞状态。

eg:

#include <signal.h>

sigset_t set;
sigaddset(&set, SIGINT); // 将SIGINT信号添加到信号集中

d.sigdelset(sigset_t *set, int signum):该函数用于将指定信号signum从信号集set中删除,将指定信号设置为未阻塞状态。

eg:

#include <signal.h>

sigset_t set;
sigdelset(&set, SIGINT); // 将SIGINT信号从信号集中删除

e.int sigismember(const sigset_t *set, int signum):用于判断指定信号是否属于set信号集。

sigismember 函数返回一个整型值,如果指定的信号 signum 包含在 set 所指向的信号集合中,则返回非零值;否则返回 0

为0表示默认的行为

sa_flags:当sa_flags中包含 SA_RESETHAND时,接受到该信号并调用指定的信号处理函数执行之后,把该信号的响应行为重置为默认行为SIG_DFL

代码示例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
}

int main(void) 
{
	struct sigaction act;

	act.sa_handler = myhandle;
	sigemptyset(&act.sa_mask);
       act.sa_flags = 0;

	sigaction(SIGINT, &act, 0);

	while (1) {
        
	}

	return 0;
}

信号的发送

信号的发送方式:

      1.在shell终端用快捷键产生信号

      2.使用kill,killall命令。

      3.使用kill函数和alarm函数

使用kill函数发命令
函数原型
#include <signal.h>
int kill(pid_t pid, int sig);

向进程号为pid的进程发送sig信号

返回值

成功时返回0,

失败时返回-1,并设置errno

代码示例

实例:main6.c创建一个子进程,子进程每秒中输出字符串“child process work!",父进程等待用户输入,如果用户按下字符A, 则向子进程发信号SIGUSR1, 子进程的输出字符串改为大写; 如果用户按下字符a, 则向子进程发信号SIGUSR2, 子进程的输出字符串改为小写

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int workflag = 0;

void work_up_handle(int sig) 
{
	workflag = 1;
}

void work_down_handle(int sig) 
{
	workflag = 0;
}



int main(void) 
{
	pid_t pd;
	char c;


	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		char *msg;
		struct sigaction act; 
		act.sa_flags = 0;
		act.sa_handler = work_up_handle;
		sigemptyset(&act.sa_mask);		
		sigaction(SIGUSR1, &act, 0);
		
		act.sa_handler = work_down_handle;
		sigaction(SIGUSR2, &act, 0);
		
		while (1) {
			if (!workflag) {
				msg = "child process work!";
			} else {
				msg = "CHILD PROCESS WORK!";
			}
			printf("%s\n", msg);
			sleep(1);
		}
		
	} else {
		while(1) {
			c = getchar();
			if (c == 'A') {
				kill(pd, SIGUSR1);
			} else if (c == 'a') {
				kill(pd, SIGUSR2);
			}
		}
	}
	

	return 0;
}

实例:main7.c “闹钟”,创建一个子进程,子进程在5秒钟之后给父进程发送一个SIGALR,父进程收到SIGALRM信号之后,“闹铃”(用打印模拟)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int wakeflag = 0;

void wake_handle(int sig) 
{
	wakeflag = 1;
}

int main(void) 
{
	pid_t pd;
	char c;


	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		sleep(5);
		kill(getppid(), SIGALRM);
	} else {
		struct sigaction act; 
		act.sa_handler = wake_handle;
		act.sa_flags = 0;
		sigemptyset(&act.sa_mask);

		sigaction(SIGALRM,  &act, 0);

		pause(); //把该进程挂起,直到收到任意一个信号

		if (wakeflag) {
			printf("Alarm clock work!!!\n");
		}
	}

	return 0;
}

上面代码中用到了pause函数:

pause函数

函数原型

#include <unistd.h>

int pause(void);

调用pause()函数会使当前进程挂起,并等待信号到来。当进程接收到一个信号时,pause()函数返回 -1,并设置errnoEINTR,表示被信号打断。

使用alarm函数发送信号——进程自己给自己发送SIGALRM信号
函数原型
#include <unistd.h>

unsigned int alarm(unsigned int seconds);
函数功能

 调用alarm(seconds)函数会设置一个定时器,在经过seconds秒后,向当前进程发送一个SIGALRM信号。如果之前已经设置过定时器,新的定时器将会覆盖之前的定时器。

返回值

如果之前已经设置过定时器,则返回上一个定时器剩余时间未到期的秒数;如果之前没有设置过定时器,则返回0.

注意:时间的单位是“秒”

                 实际闹钟时间比指定的时间要大一点。  

                 如果参数为0,则取消已设置的闹钟。

                 如果闹钟时间还没有到,再次调用alarm,则闹钟将重新定时

                 每个进程最多只能使用一个闹钟。

raise函数——进程给自己发送信号(用的比较少)

函数原型

#include<signal.h>
int raise(int sig);

发送多个信号
重复收到同一信号

则:如果该信号是不可靠信号(<32),则只能再响应一次。

如果该信号是可靠信号(>32),则能再响应多次(不会遗漏)。但是,都是都必须等该次响应函数执行完之后,才能响应下一次。

执行信号处理函数的时候收到其他信号

    如果该信号被包含在当前信号的signaction的sa_mask(信号屏蔽集)中,则不会立即处理该信号。直到当前的信号处理函数执行完之后,才去执行该信号的处理函数。

    否则:

则立即中断当前执行过程(如果处于睡眠,比如sleep, 则立即被唤醒)而去执行这个新的信号响应。新的响应执行完之后,再在返回至原来的信号处理函数继续执行。

设置进程的信号屏蔽字——sigprocmask函数

函数原型

 #include <signal.h>

       /* Prototype for the glibc wrapper function */
       int sigprocmask(int how, const sigset_t *_Nullable restrict set,
                                  sigset_t *_Nullable restrict oldset);

参数:

             how:

                   SIG_BLOCK      把参数set中的信号添加到信号屏蔽字中

                   SIG_UNBLOCK  把参数set中的信号从信号屏蔽字中删除

                   SIG_SETMASK  把参数set中的信号设置为信号屏蔽字

             

             oldset

                  返回原来的信号屏蔽字

返回值

  • 如果函数执行成功,则返回 0。
  • 如果函数执行失败,则返回 -1,并设置 errno 变量来指示错误的原因

代码示例

main4_3.c

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
	printf("Catch end.%d\n", sig);
}

int main(void) 
{
	struct sigaction act, act2;

	act.sa_handler = myhandle;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	sigaction(SIGINT, &act, 0);

	sigset_t proc_sig_msk, old_mask;
	sigemptyset(&proc_sig_msk);
	sigaddset(&proc_sig_msk, SIGINT);

	sigprocmask(SIG_BLOCK, &proc_sig_msk, &old_mask);
	sleep(5);
	printf("had delete SIGINT from process sig mask\n");
	sigprocmask(SIG_UNBLOCK, &proc_sig_msk, &old_mask);
		
	while (1) {
		
	}

	return 0;
}

                  

获取未处理的新型号——sigpending函数

 当进程的信号屏蔽字中信号发生时,这些信号不会被该进程响应,

  可通过sigpending函数获取这些已经发生了但是没有被处理的信号

函数原型

#include <signal.h>

       int sigpending(sigset_t *set);

返回值

成功返回0,失败返回-1

阻塞式等待信号

 (1) pause

            阻塞进程,直到发生任一信号后

       

       (2) sigsuspend

            用指定的参数设置信号屏蔽字,然后阻塞时等待信号的生。

            即,只等待信号屏蔽字之外的信号

 #include <signal.h>

       int sigsuspend(const sigset_t *mask);

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

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

相关文章

dledger原理源码分析(四)-日志

简介 dledger是openmessaging的一个组件&#xff0c; raft算法实现&#xff0c;用于分布式日志&#xff0c;本系列分析dledger如何实现raft概念&#xff0c;以及dledger在rocketmq的应用 本系列使用dledger v0.40 本文分析dledger的日志&#xff0c;包括写入&#xff0c;复制…

esp32硬件电路设计

ESP-IDF 入门指南 | 乐鑫科技 (espressif.com) ESP32-DevKitC V4 入门指南 - ESP32 - — ESP-IDF 编程指南 v5.1 文档 (espressif.com)

看惯了黑黝黝的大屏风格再来看浅色系的大屏,很漂亮很个性

**看惯了黑黝黝的大屏风格&#xff0c;再来看浅色系的大屏&#xff0c;很漂亮很个性** 在科技产品的世界里&#xff0c;大屏设计一直以其沉浸感和视觉冲击力占据着一席之地。然而&#xff0c;当我们长时间沉浸在那些深邃、沉稳的黑黝黝大屏中时&#xff0c;是否曾想过换一种风…

VBA即用型代码手册:根据预定义的文本条件删除行

我给VBA下的定义&#xff1a;VBA是个人小型自动化处理的有效工具。可以大大提高自己的劳动效率&#xff0c;而且可以提高数据的准确性。我这里专注VBA,将我多年的经验汇集在VBA系列九套教程中。 作为我的学员要利用我的积木编程思想&#xff0c;积木编程最重要的是积木如何搭建…

uni-app三部曲之三: 路由拦截

1.引言 路由拦截&#xff0c;个人理解就是在页面跳转的时候&#xff0c;增加一级拦截器&#xff0c;实现一些自定义的功能&#xff0c;其中最重要的就是判断跳转的页面是否需要登录后查看&#xff0c;如果需要登录后查看且此时系统并未登录&#xff0c;就需要跳转到登录页&…

电子电气架构 --- 智能座舱万物互联

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

windows防火墙端口设置

PS&#xff1a;本文实例为Windows Server 2019&#xff0c;其他Windows版本大同小异。 1、首先打开windows防火墙&#xff0c;点击“高级设置” 2、 高级设置界面 3、假设需要开放一个端口为3306应该怎么做 光标对准“入站规则”右键新建规则&#xff0c;选择“端口” 协议这…

【UE5.1 角色练习】16-枪械射击——瞄准

目录 效果 步骤 一、瞄准时拉近摄像机位置 二、瞄准偏移 三、向指定方向射击 四、连发 效果 步骤 一、瞄准时拉近摄像机位置 打开角色蓝图&#xff0c;在事件图表中添加如下节点&#xff0c;当进入射击状态时设置目标臂长度为300&#xff0c;从而拉近视角。 但是这样切…

Android 通知访问权限

问题背景 客户反馈手机扫描三方运动手表&#xff0c;下载app安装后&#xff0c;通知访问权限打不开。 点击提示“受限设置” “出于安全考虑&#xff0c;此设置目前不可用”。 问题分析 1、setting界面搜“授予通知访问权限”&#xff0c;此按钮灰色不可点击&#xff0c;点…

C++基础篇(1)

目录 前言 1.第一个C程序 2.命名空间 2.1概念理解 2.2namespace 的价值 2.3 namespace的定义 3.命名空间的使用 4.C的输入输出 结束语 前言 本节我们将正式进入C基础的学习&#xff0c;话不多说&#xff0c;直接上货&#xff01;&#xff01;&#xff01; 1.第一个C程…

JAVA分布式事务详情分布式事务的解决方案Java中的分布式事务实现

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:王文峰…

关于Python中的字典你所不知道的七个技巧

01 引言 Python是我最喜欢的编程语言之一&#xff0c;它向来以其简单性、多功能性和可读性而闻名。 字典作为Python中最常使用的数据类型&#xff0c;大家几乎每个人都或多或少在项目中使用过字典&#xff0c;但是字典里有一些潜在的技巧可能并不是每个同学都会用到。 在本文…

院内导航:如何用科技破解就医找路难题

自2019年开始“院内导航”被纳入医院智慧服务评估体系以来&#xff0c;到2023年改善就医服务升级的部署&#xff0c;每一步都见证了我国医疗卫生体系向智能化、人性化迈进的坚实步伐。 面对庞大复杂的医院环境与日益增长的就诊需求&#xff0c;如何让患者在茫茫人海中迅速找到就…

31_JQuery一文读懂,JS的升级版

今日内容 零、 复习昨日 一、JQuery 零、 复习昨日 1 js数组的特点(长度,类型,方法) - js数组的长度不限 - 类型不限 - 提供很多方法2 js中和的区别 - 判断数值相等 - 判断数值和数据类型同时相等3 js表单事件的事件名(事件属性单词) - 获得焦点 onfocus - 失去焦点 onblur …

干货:XXX智慧城市大数据共享交换平台建设方案(145页word)

引言&#xff1a;智慧城市大数据共享交换平台建设方案旨在构建一个高效、安全、可扩展的数据共享与交换生态系统&#xff0c;以促进城市内不同部门、机构及企业间的数据互联互通&#xff0c;推动数据资源的深度整合与利用&#xff0c;加速智慧城市建设进程。 方案介绍&#xff…

TongRDS 2214 docker版指引(by lqw )

文章目录 前言准备工作中心节点服务节点哨兵节点 前言 部署docker版本&#xff0c;建议先参考TongRDS2214手动部署版指引&#xff08;by lqwsy&#xff09; 在本地手动部署了一套适合业务场景的rds 服务后&#xff0c;再通过dockerfile 打镜像。 准备工作 1.准备对应的安装包…

开始性能测试之前的准备工作!

性能测试是软件测试中不可或缺的一部分&#xff0c;它可以帮助我们评估软件系统的性能表现&#xff0c;并找出潜在的性能瓶颈。在进行性能测试之前&#xff0c;需要做好充分的准备工作&#xff0c;以确保测试的有效性和准确性。 1. 确定性能测试的目标和范围 * 明确测试目标:性…

网口变压器下方是否要挖空探讨

一. 引言 1. RJ45组成 RJ45模块用于PHY芯片之间的互连&#xff0c;如图1所示&#xff0c;RJ45有两种组合形式&#xff0c;一种是分立式&#xff0c;网络变压器和RJ45连接座是分开的&#xff0c;另一种是网络变压器和RJ45集成在一起。RJ45连接座又分为带屏蔽与不带屏蔽两种。 …

C++基础编程100题-021 OpenJudge-1.4-01 判断数正负

更多资源请关注纽扣编程微信公众号 http://noi.openjudge.cn/ch0104/01/ 描述 给定一个整数N&#xff0c;判断其正负。 输入 一个整数N(-109 < N < 109) 输出 如果N > 0, 输出positive; 如果N 0, 输出zero; 如果N < 0, 输出negative 样例输入 1样例输出…

MySQL空间索引

空间类型是建立在空间类型字段上的。 空间类型 MySQL的空间类型很多&#xff0c;我就不逐一介绍了。重要分四大类&#xff1a; GeometryCurveSurfaceGeometryCollection 前三种&#xff0c;地理、曲线、曲面都是不可实例化的。Geometry有一个子类Point, Curve有一个直接子类L…