进程之信号

news2024/9/21 11:19:04

文章目录

  • 进程信号
    • 中断的概念
    • 信号是异步事件
    • Linux信号
      • 信号发生的来源
      • 信号的处理方式
      • signal函数
        • 示例--使用signal函数对常见的信号进行捕获、执行默认操作、忽略三种方式处理
      • SIGCHLD信号
        • 示例--使用SIGCHLD信号来避免子进程退出
      • kill函数
      • raise函数
        • 示例--使用kill函数给指定进程发送信号
        • 示例--使用kill函数和raise函数向当前进程发送信号
        • 示例--使用`kill`和`signal`函数练习
    • 定时器
      • alarm函数
        • 示例--使用alarm函数设置一个定时器
        • 示例--使用定时器每隔一个定时时间翻转引脚状态
      • setitimer函数
        • 示例--使用setitimer函数设置定时器

进程信号

信号是Linux系统中的一种通信机制,常见的通信机制还有文件锁、管道、消息队列、共享内存等。

中断的概念

  • 硬件中断

    • 在单片机中常常能看到有硬件中断,中断说白了就是一种机制,中断的存在允许当程序在运行的时候能够转而去执行别的操作,在单片机中,当发生硬件中断的时候,CPU首先会停下当前的工作,然后将寄存器等状态保存在堆栈中,然后转而去执行中断服务程序。当程序执行完以后,继续返回之前被中断的地方继续执行。
  • 软件中断

    • 软件中断就是通过一种信号来出发它的中断,例如这里的信号。信号作为一种软件中断它的原理是,当一个进程给另外一个进程发送信号的时候,另外一个进程停下当前正在做的事情,然后转而去处理信号函数。但是这种是通过信号机制来中断当前正在执行的程序,而非是由于硬件中断所引起的,所以信号是一种软件中断

信号是异步事件

  • 异步事件:异步事件就是说当进程1发送信号给进程2以后不管去管对方到底有没有接收到,进程1都要往下执行,这种异步操作能够通过减少等待时间来提高程序的响应性和性能。
  • 同步事件:以这里的信号为例,如果是同步事件的话,当进程1给进程2发信号以后必须收到来自进程2的回复进程1才能够继续往下执行。

异步事件和同步事件对比:经过对比发现异步事件比同步实践的执行效率更好,但是可能执行精度上较低于同步事件。同步事件虽然资源利用效率低下,但是能够保持较高的精度,一旦出错以及停止执行然后进行相应的处理。所以对于两种方式的选择要进行合理的选择才能达到预期的效果。

Linux信号

在Linux系统中的信号都是继承了Unix系统的信号,在Linux系统中可以使用kill -l来查看系统中的所有信号。

image-20240909165055152

信号发生的来源

  • 硬件来源:当按下键盘的Ctrl+CCtrl+Z的时候会产生SIGINTSIGTSTP信号,这些信号都是由硬件驱动程序产生,当内核的驱动程序捕捉到硬件按下对应的按键以后从而触发对应的信号。
    • 软件来源:最常用发送信号的系统函数是kill()raise()alarm()setitimer()等函数,软件来源还包括一些非法运算等操作(如除零操作、段错误),软件设置条件(如:gdb调试),信号由内核产生。

信号的处理方式

  • 忽略信号

    • SIGKILLSIGSTOP永远不能忽略
    • 进程启动时SIGUSR1SIGUSR2两个信号被忽略
    • 这里的忽略信号需要使用一个宏SIG_IGN来将指定的信号忽略
  • 执行默认操作

    • 每个信号都有默认动作,大部分信号的默认动作是终止进程
    • 执行默认操作使用SIG_DFL宏来将指定的信号执行默认操作
  • 捕获信号

    • 使用signal()函数来将指定的信号捕获,然后转而去执行自己定义的信号处理函数
    • SIGKILLSIGSTOP不能被捕获

    SIGKILLSIGSTOP信号不能被忽略或捕获的原因是:它们两个相当于一种保护机制,当Ctrl+C(SIGINT)Ctrl+Z(SIGTSTP)等信号被捕获转而去执行用户自定义的函数时,必须保证有一个信号能够将此进程杀死或者停止,所以这里SIGKILLSIGSTOP的底层就写好了它们不能被忽略也不能被捕获。

signal函数

上边说的几种信号的处理方式可以通过Linux系统提供的一个函数来实现:

#include <signal.h>

void (*signal(int sig, void (*func)(int)))(int);

//功能:向内核注册信号处理函数,当发生对应的信号时做出相应的操作
//参数1:要捕获的信号
//参数2:函数指针,指向用户自定义的信号处理函数
//返回值:如果成功执行,将返回一个指向先前信号处理函数的指针。若执行失败,则返回值为SIG_ERR

这里对signal函数的原型进行详解:

  • signal函数的第一个参数sig是要捕捉的信号,例如SIGINT SIGTSTP等,然后对捕捉到的信号进行相应的操作,这个操作函数就是它的第二个参数。这里的sig可以用编号来表示也可以用它具体的信号来表示(如SIGINT信号对应的编号是2)
  • signal函数的第二个参数void (*func)(int)是一个函数指针,它是使用typedef定义的类型别名,这个函数指针指向了一个函数,该函数是一个无返回值,具有一个整型参数的函数。
  • 它的第二个参数还可以是两个宏:SIG_INGSIG_DFL分别表示忽略信号和执行该信号的默认操作
  • signal函数的返回值是一个函数指针,它指向了一个函数,该函数是一个无返回值,具有一个整型参数的函数。实际上它的返回值就是信号处理函数。
示例–使用signal函数对常见的信号进行捕获、执行默认操作、忽略三种方式处理
#include "header.h"

void sig_handler(int signum)
{
    switch(signum)
    {
        case 1:
            printf("process %d catches signal is SIGHUP\n",getpid());
            break;
        case 2:
            printf("process %d catches signal is SIGINT\n",getpid());
            break;
        case 9:
            printf("process %d catches signal is SIGKILL\n",getpid());
            break;
      	case 10:
            printf("process %d catches signal is SIGUSR1\n",getpid());
            break;
        case 12:
            printf("process %d catches signal is SIGUSR2\n",getpid());
            break;
        case 19:
            printf("process %d catches signal is SIGSTOP\n",getpid());
            break;
        default:
            printf("there is no such signal\n");
    }
}

int main(void)
{
	printf("process pid is %d\n",getpid());

    //向内核登记信号处理函数以及需要捕获的信号
    if(signal(SIGINT, sig_handler) == SIG_ERR)
    {
        perror("signal SIGINT error");
    }
    
    if(signal(SIGHUP, sig_handler) == SIG_ERR)
    {
        perror("signal SIGHUP error");
    }
    
    if(signal(SIGUSR1, sig_handler) == SIG_ERR)
    {
        perror("signal SIGUSR1 error");
    }
    
    if(signal(SIGUSR2, sig_handler) == SIG_ERR)
    {
        perror("signal SIGUSR2 error");
    }
    
    if(signal(SIGKILL, sig_handler) == SIG_ERR)
    {
        perror("signal SIGKILL error");
    }
    
    if(signal(SIGSTOP, sig_handler) == SIG_ERR)
    {
        perror("signal SIGSTOP error");
    }
    
    while(1) sleep(1);
    
    return 0;
}

image-2024091019742

通过编译执行,发现signal函数将SIGHUPSIGINT信号捕获,而且执行了用户自定义的函数,这里由于SIGHUP函数不能通过键盘等硬件方式去向内核发送信号,所以采用kill指令去发送信号。这里使用kill给进程发送编号为3的信号,程序退出,由此也验证了大部分信号的默认处理机制是将程序结束。

在这里插入图片描述

当使用signal函数去捕获SIGKILLSIGSTOP信号的时候,可以看到这里在运行的时候就直接报错了,显示非法的参数。并且使用SIGKILLSIGSTOP信号还能杀死和停止进程。SIGUSR1SIGUSR2信号虽然在进程启动的时候被忽略了,但是可以使用signal函数对它们进行捕获执行相应的函数。

如果这里将signal函数的第二个参数设置为SIG_IGNSIG_DFL的话,它们分别会将捕获的信号忽略和执行默认操作,例如这里将SIGINT信号设置为默认的操作,它就会将正在运行的进程中断。如果将signal函数设置为SIG_IGN它的处理是将信号忽略不做任何处理。

image-20240910171827116

image-20240910171954422

SIGCHLD信号

前边进程那一章有讲过当子进程优于父进程退出的时候,子进程的资源无法完全释放,在内核中还留有进程表项。所以当时为了避免子进程成为僵尸进程共有三种方法:

  • 在父进程中使用wait函数等待子进程退出,然后让父进程通知内核去回收子进程的进程表项;
  • 当运行起来的时候,将父进程杀死。使子进程变成一个孤儿进程然后由initsystemd进程收留,最后由它们去释放子进程的资源;
  • 当子进程退出的时候会产生SIGCHLD信号,此时如果父进程捕获到子进程发送此信号就表明子进程退出了,调用wait函数去回收子进程的资源。

通过前边的案例不难发现当父进程调用wait函数以后父进程就处于阻塞状态了,所以父进程一直在等待子进程退出,中间是不能做任何事情的,这其实是资源的浪费。所以这里介绍一些应该怎么使用SIGCHLD信号来避免子进程变成僵尸进程,通过在使用SIGCHLD信号的时候父进程也不是处于阻塞状态,父进程可以干自己的事,相比之前直接使用wait函数等待子进程退出效果更好一点。

示例–使用SIGCHLD信号来避免子进程退出
#include "header.h"

void sig_handler(int signum)
{
    printf("process %d receive a signal and the number is %d\n",getpid(),signum);
    wait(NULL);
}

void count_func(int num)
{
    int i;
    
    for(i=0;i<num;i++)
    {
        printf("process: %d i:%d\n",getpid(),i);
        sleep(1);
    }
}

int main(void)
{
    //向内核登记捕获SIGCHLD信号,一旦产生这个信号就去执行相应的信号处理函数
    if(signal(SIGCHLD, sig_handler) == SIG_ERR)
    {
        perror("signal SIGCHLD error");
    }
    
    pid_t pid;
    if((pid = fork()) < 0)
    {
        perror("fork error");
        exit(EXIT_FAILURE);
    }
    else if(pid == 0)
    {
        printf("this is child process and the pid is %d\n",getpid());
        count_func(10);
    }
    else
    {
        printf("this is parent process and the pid is %d\n",getpid());
        count_func(40);
    }
    
    return 0;
}

image-20240910175255586

通过编译执行可以发现,在子进程退出的时候会发送一个SIGCHLD信号,在此时调用wait函数可以将子进程的资源进行回收,使用ps指令去查看子进程状态也没有变成僵尸进程,说明子进程成功被回收。

kill函数

在上边的终端里使用kill指令发送信号给特定的进程,在Linux系统里又一个kill函数和kill指令的功能是一样的。

#include <signal.h>

int kill(pid_t pid, int sig);

//功能:向指定进程发送信号
//参数1:指定进程的PID号
//参数2:向指定进程发送的信号
//返回值:如果成功执行返回0,否则返回非零值并设置errno

raise函数

#include <signal.h>

int raise(int sig);

//功能:向执行此函数的当前进程发送指定的信号
//参数:要发送的信号
//返回值:如果成功执行返回0,否则返回非零值并设置errno
示例–使用kill函数给指定进程发送信号
#include "header.h"

int main(int argc, char **argv)
{
	if(argc < 3)
	{
		fprintf(stderr,"usage: %s [pid] [signum]\n",argv[0]);
		exit(EXIT_FAILURE);
	}

	//从外部传入要发送进程的PID号和信号编号
	pid_t pid = atoi(argv[1]);
	int signum = atoi(argv[2]);

	if((kill(pid,signum)) != 0)
	{
		perror("kill perror");
		exit(EXIT_FAILURE);
	}

	return 0;
}

在这里插入图片描述

经过代码编译执行发现kill函数的使用和kill指令无异,都可以使用它给指定进程发送信号。

示例–使用kill函数和raise函数向当前进程发送信号
#include "header.h"

void sig_handler(int signum)
{
    printf("process %d receive a signal is %d\n",getpid(),signum);

    switch (signum)
    {
    case 1:
        printf("receive signal is SIGHUP\n");
        break;
    case 2:
        printf("receive signal is SIGINT\n");
        break;
    case 10:
        printf("receive signal is SIGUSR1\n");
        break;
    case 12:
        printf("receive signal is SIGUSR2\n");
        break;
    default:
        printf("no sign to kernel\n");
        break;
    }
}

int main(int argc, char **argv)
{
    if(argc < 2)
    {
        fprintf(stderr,"usage: %s [signum]\n",argv[0]);
        exit(EXIT_FAILURE);
    }

    int signum = atoi(argv[1]);

    if(signal(SIGUSR1, sig_handler) == SIG_ERR)
    {
        perror("signal SIGUSR1 error");
    }

    if(signal(SIGUSR2, sig_handler) == SIG_ERR)
    {
        perror("signal SIGUSR2 error");
    }

    if(signal(SIGHUP, sig_handler) == SIG_ERR)
    {
        perror("signal SIGHUP error");
    }

    if(signal(SIGINT, sig_handler) == SIG_ERR)
    {
        perror("signal SIGINT error");
    }

    //将当前进程的PID号传进去给当前进程发信号
    if(kill(getpid(), signum) != 0)
    {
        perror("kill perror");
        exit(EXIT_FAILURE);
    }

    if(raise(signum) != 0)
    {
        perror("raise perror");
        exit(EXIT_FAILURE);
    }
    
    return 0;
}

13f5d04b8a676d58dc79d33b8aee0a0

通过编译执行可以发现kill函数可以向当前进程发送信号,kill(getpid(), signum)这个等同于raise(signum),它们两个都可以向当前进程发送信号。

这里有一个比较有意思的案例:父进程使用fork函数创建子进程,在子进程中调用信号处理函数,然后在父进程中使用kill函数向子进程发送信号

示例–使用killsignal函数练习
#include "header.h"

void sig_handler(int signum)
{
    printf("pid:%d signum:%d\n",getpid(),signum);
}

int main()
{
    pid_t pid;
    
    if((pid = fork()) < 0)
    {
        perror("fork error");
        exit(EXIT_FAILURE);
    }
	else if(pid == 0)	//子进程向内核注册信号处理函数
	{
		printf("this is child process,pid:%d ppid:%d\n",getpid(),getppid());

		if(signal(SIGHUP, sig_handler) == SIG_ERR)
		{
			perror("signal error");
		}
		
		int i = 0;
		while(i < 15)
		{
			printf("pid:%d i = %d\n",getpid(),i);
			i++;
			sleep(1);
		}
	}
	else		//父进程向子进程发送信号
	{
		printf("parent process,pid:%d child's pid:%d\n",getpid(),pid);
		if(kill(pid, SIGHUP) != 0)
		{
			perror("kill error");
			exit(EXIT_FAILURE);
		}
		wait(NULL);
	}
}

image-20240911180952696

通过编译执行,发现并没有想象的父进程向子进程发送信号,然后子进程捕获对应的信号执行相应的操作。实际的运行效果是父进程执行,子进程直接退出了。这里先说一下原因:当父进程先运行的时候,就会打印父进程和子进程的pid,然后通过kill函数给子进程发送信号。通过上边的内容可知信号是一种异步机制,所以这里父进程给子进程发送信号已经发送成功了,只不过这时候子进程可能还没有调用signal函数向内核登记信号处理函数,所以父进程向子进程发送的信号执行它的默认操作。由于大部分信号的默认操作都是结束该进程,所以这里的子进程直接被结束掉了,这也就是子进程为什么没有运行的原因。所以这里要想让子进程收到来自父进程的信号就要让子进程先开始运行,让父进程后运行,保证子进程已经向内核登记了信号处理函数以后才让父进程发送信号给子进程。只需在父进程开始时加一句sleep(1)就可以实现这个功能,修改后的执行结果如下:

image-20240911182500727

定时器

在Linux系统中有一种定时器信号SIGALRM,用于通知进程一个定时器已经到期。当设置了一个定时器(使用setitimer()alarm()函数),并且时间到达时,操作系统会向进程发送SIGALRM信号。进程可以选择忽略这个信号,或者捕获它并执行相应的处理程序。

alarm函数

#include <unistd.h>

unsigned alarm(unsigned seconds);

//功能:用于在指定的时间后由内核向调用进程发送SIGALRM信号
//参数:以秒为单位的要设定的时间
//返回值:返回0或以前设置的定时器时间余留秒数
//注意:alarm这个函数是一次性的,调用一次只产生一次alarm信号,如果想要产生周期性的信号,要在信号处理函数里再次调用
示例–使用alarm函数设置一个定时器
#include "header.h"

void sig_handler(int signum)
{
	if(signum == 14)
		printf("receive a signal is SIGALRM,time out\n");
	alarm(5);		//当进入到这个函数里就说明触发了SIGALRM信号,此时要再次设置定时时间来产生周期性的定时时间
}

int main(void)
{
	pid_t pid;

	//向内核登记SIGALRM信号处理函数,如果产生了这个信号就去执行相应的信号处理函数
	if(signal(SIGALRM, sig_handler) == SIG_ERR)
	{
		perror("signal error");
		exit(EXIT_FAILURE);
	}

	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(EXIT_FAILURE);
	}
	else if(pid == 0)
	{
		printf("child's pid:%d ppid:%d\n",getpid(),getppid());
	
		int i = 0;

		while(i < 15)
		{
			printf("pid:%d i = %d\n",getpid(),++i);
			sleep(1);
		}	
	}
	else
	{
		printf("parent's pid:%d child's pid:%d\n",getpid(),pid);
		alarm(5);		//设定5秒后产生SIGALRM信号
		wait(NULL);
	}

	return 0;
}

image-20240911201157312

通过编译执行,可以发现通过alarm()函数可以来设置一个定时器,功能和单片机的硬件定时器类似。到时间以后就产生SIGALRM信号,由于之前使用signal函数向内核登记了该信号和信号处理函数,所以当该信号产生的时候就去信号处理函数里进行相应的操作。

示例–使用定时器每隔一个定时时间翻转引脚状态
#include "header.h"
#include <wiringPi.h>

#define PIN 1

void sig_handler(int signum)
{
	static int i = 0;
	if(signum == 14)
		printf("receive a signal is SIGALRM,time out\n");
	alarm(5);		//当进入到这个函数里就说明触发了SIGALRM信号,此时要再次设置定时时间来产生周期性的定时时间

	if(++i % 2 == 0)		//当i对2取余等于0的时候,将引脚设置为高电平反之设置为低电平
		digitalWrite(PIN,HIGH);
	else
		digitalWrite(PIN,LOW);
}

int main(void)
{
	pid_t pid;

	//向内核登记SIGALRM信号处理函数,如果产生了这个信号就去执行相应的信号处理函数
	if(signal(SIGALRM, sig_handler) == SIG_ERR)
	{
		perror("signal error");
		exit(EXIT_FAILURE);
	}

	if(wiringPiSetup() == -1)		//初始化wiringPi库
	{
		perror("init wiring error");
		exit(EXIT_FAILURE);
	}

	//将引脚配置为输出模式
	pinMode(PIN,OUTPUT);

	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(EXIT_FAILURE);
	}
	else if(pid == 0)
	{
		printf("child's pid:%d ppid:%d\n",getpid(),getppid());
	
		int i = 0;

		while(i < 15)
		{
			printf("pid:%d i = %d\n",getpid(),++i);
			sleep(1);
		}	
	}
	else
	{
		printf("parent's pid:%d child's pid:%d\n",getpid(),pid);
		alarm(5);		//设定5秒后产生SIGALRM信号
		wait(NULL);
	}

	return 0;
}

image-20240911203806593

由于这里用到了wiringPi库,所以要将-lwiringPi选项,执行的时候要用超级用户权限

image-20240911203912909image-20240911203945014image-20240911204014066

这里使用gpio readall这个指令来查看引脚的输入输出模式以及引脚的电平状态,通过对比发现它每五秒切换一次引脚号为1的电平状态,通过alarm函数实现了定时器的用法。

setitimer函数

#include <sys/time.h>

int setitimer(int which, const struct itimerval *restrict value,struct itimerval *restrict ovalue);

struct itimerval {
               struct timeval it_interval; /* Interval for periodic timer */
               struct timeval it_value;    /* Time until next expiration */
           };
struct timeval {
               time_t      tv_sec;         /* seconds */
               suseconds_t tv_usec;        /* microseconds */
           };

//功能:用于设置定时器,以实现延时和定时功能。该函数可以代替alarm函数,并具有更高的精度,支持微秒级的定时控制,它的工作机制是在指定的时间后发送特定的信号(如SIGALRM、SIGVTALRM或SIGPROF)给进程。
//参数1:which参数用于指定定时器的类型,
	//ITIMER_REAL:真实时间计时器,以系统实时时间为准,一旦时间到达就发送信号。
	//ITIMER_VIRTUAL:虚拟时间计时器,以进程在用户态消耗的时间为准。
	//ITIMER_PROF:CPU时间计时器,以进程在用户态和内核态所消耗的总时间为准。
//参数2:新的定时器设置值(value):这是一个指向struct itimerval结构体的指针,该结构体包含了定时器的间隔时间和总时间。
	//it_value:首次触发定时器前的等待时间。(定时器开始工作前需要的时间)
	//it_interval:定义了两次触发之间的时间间隔。如果设置为0,则定时器只触发一次。(要设定的时间)
	//在struct itimerval中又嵌入了一个结构体,这两个结构体的成员分别是tv_sec和tv_usec,用来设置秒和微秒。
//参数3:旧的定时器设置值(old_value):这也是一个指向struct itimerval结构体的指针,用来存储当前定时器的设置值。如果不需要获取当前值,可以将其设置为NULL。
//返回值:setitimer函数的返回值0表示成功,返回-1表示出错,出错时可以通过errno变量获取具体的错误信息。
示例–使用setitimer函数设置定时器
#include "header.h"
#include <wiringPi.h>
#include <sys/time.h>

#define PIN 1

void sig_handler(int signum)
{
	static int i = 0;
	if(signum == 14)
		printf("receive a signal is SIGALRM,time out\n");

	if(++i % 2 == 0)		//当i对2取余等于0的时候,将引脚设置为高电平反之设置为低电平
		digitalWrite(PIN,HIGH);
	else
		digitalWrite(PIN,LOW);
}

int main(void)
{
	pid_t pid;

	//向内核登记SIGALRM信号处理函数,如果产生了这个信号就去执行相应的信号处理函数
	if(signal(SIGALRM, sig_handler) == SIG_ERR)
	{
		perror("signal error");
		exit(EXIT_FAILURE);
	}

	if(wiringPiSetup() == -1)		//初始化wiringPi库
	{
		perror("init wiring error");
		exit(EXIT_FAILURE);
	}

	//将引脚配置为输出模式
	pinMode(PIN,OUTPUT);

	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(EXIT_FAILURE);
	}
	else if(pid == 0)
	{
		printf("child's pid:%d ppid:%d\n",getpid(),getppid());
	
		int i = 0;

		while(i < 15)
		{
			printf("pid:%d i = %d\n",getpid(),++i);
			sleep(1);
		}	
	}
	else
	{
		printf("parent's pid:%d child's pid:%d\n",getpid(),pid);

		struct itimerval timer;
		timer.it_interval.tv_sec = 5;		//设定定时时间,支持微秒级
		timer.it_interval.tv_usec = 0;
		timer.it_value.tv_sec = 5;			//设定定时器在启动前经过的时间
		timer.it_value.tv_usec = 0;
		if(setitimer(ITIMER_REAL,&timer,NULL) == -1)		//设置定时器参数,使用真实时间计时器进行定时
		{
			perror("setitimer error");
			exit(EXIT_FAILURE);
		}
		
		wait(NULL);
	}

	return 0;
}

image-20240911213636185

image-20240911213715782image-20240911213738375image-20240911213809585

通过上边的编译结果可以看出setitimer函数也可以用来设置定时器,相比alarm函数来说,它还支持毫秒级的定时,使用起来更加精确一点。

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

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

相关文章

基于python+django+vue的学生成绩管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于协同过滤pythondjangovue…

Java之线程篇四

目录 volatile关键字 volatile保证内存可见性 代码示例 代码示例2-&#xff08;volatile&#xff09; volatile不保证原子性 synchronized保证内存可见性 wait()和notify() wait()方法 notify() 理解notify()和notifyAll() wait和sleep的对比 volatile关键字 volati…

【C++ Primer Plus习题】16.3

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: #include <iostream> #include <string> #include <…

Datawhale------Tiny-universe学习笔记——Qwen(1)

1. Qwen整体介绍 对于一个完全没接触过大模型的小白来说&#xff0c;猛一听这个名字首先会一懵&#xff1a;Qwen是啥。这里首先解答一下这个问题。下面是官网给出介绍&#xff1a;Qwen是阿里巴巴集团Qwen团队研发的大语言模型和大型多模态模型系列。其实随着大模型领域的发展&a…

Linux服务器上安装git lfs命令

有时候&#xff0c;需要批量下载数据集时要用到git lfs命令 首先&#xff0c;使用pip install git-lfs安装&#xff0c;会发现使用时仍然提示&#xff1a;git: lfs is not a git command. See git --help. 这就意味着安装不成功。 因此&#xff0c;需要通过如下途径手动安装&a…

基于YOLOv5的农作物叶片病害识别系统

植物农作物叶片病虫害识别系统&#xff1a;农作物叶片病害AI检测与识别系统 源码 带UI界面说明视频 模型&#xff1a;yolov5 功能: 农作物叶片病害检测系统用于智能检测常见农作物叶片病害情况&#xff0c;自动化标注、记录和保存病害位置和类型&#xff0c;辅助作物病害防治以…

【Motion Forecasting】【摘要阅读】BANet: Motion Forecasting with Boundary Aware Network

BANet: Motion Forecasting with Boundary Aware Network 这项工作发布于2022年&#xff0c;作者团队来自于OPPO。这项工作一直被放在arxiv上&#xff0c;并没有被正式发表&#xff0c;所提出的方法BANet在2022年达到了Argoverse 2 test dataset上的SOTA水准。 Method BANet…

用Python解决综合评价问题_模糊综合评价,决策树与灰色关联分析

一&#xff1a;模糊综合评价 模糊综合评价是一种有效的处理不确定性和模糊性的评价方法&#xff0c;特别是在人才评价等领域。它允许我们综合考虑多个评价指标&#xff0c;并给出一个综合的评价结果。以下是利用模糊综合评价对人才进行评价的步骤&#xff1a; 确定评价指标&am…

进阶SpringBoot之异步任务、邮件任务和定时执行任务

SpringBooot 创建 Web 项目 异步任务&#xff1a; service 包下创建 AsyncService 类 Async 异步方法 Thread.sleep(3000) 停止三秒&#xff0c;捕获异常 package com.demo.task.service;import org.springframework.scheduling.annotation.Async; import org.springfram…

【MySQL】Windows下重启MySQL服务时,报错:服务名无效

1、问题描述 在终端中&#xff0c;停止、启动MySQL服务时报错&#xff1a;服务名无效 2、原因分析 1&#xff09;权限不够 如果是权限不够&#xff0c;会提示&#xff1a;系统错误5&#xff0c;拒绝访问。 2&#xff09;服务名错误 如果是服务名错误&#xff0c;会提示“…

第313题|解积分不等式题目的5种方法常用方法|武忠祥老师每日一题

解题思路&#xff1a;把多阶次积分和函数值联系起来&#xff0c;应该想到泰勒公式。 本题应该使用带有拉格朗日余项的泰勒公式&#xff1a; 方法一&#xff1a; 等式左右两边进行积分&#xff0c;右边第一项常数项不变&#xff0c;第二项&#xff08;x-1/2&#xff09;积完之…

macOS Sequoia 正式版(24A335)黑苹果/Mac/虚拟机系统镜像

“ 以下内容来自于黑果魏叔官网” 镜像特点 完全由黑果魏叔官方制作&#xff0c;针对各种机型进行默认配置&#xff0c;让黑苹果安装不再困难。系统镜像设置为双引导分区&#xff0c;全面去除clover引导分区&#xff08;如有需要&#xff0c;可以自行直接替换opencore分区文件为…

web安全测试入门

参考课程&#xff1a; 04-软件安全测试基础-网络协议基础-网络模型_哔哩哔哩_bilibili 1.软件安全测试概述 安全测试&#xff1a; 安全性测试指有关验证应用程序的安全等级和识别潜在安全性缺陷的过程 导致软件出现安全问题的主要原因或根源是软件的安全漏洞 安全漏洞&#x…

网页交互模拟:模拟用户输入、点击、选择、滚动等交互操作

目录 一、理论基础 1.1 网页交互模拟的重要性 1.2 网页交互的基本原理 二、常用工具介绍 2.1 Selenium 2.2 Puppeteer 2.3 Cypress 2.4 TestCafe 三、实战案例 3.1 模拟用户输入 3.2 模拟用户点击 3.3 模拟用户选择 3.4 模拟滚动操作 四、最佳实践与优化 4.1 代…

基于python+django+vue的学生管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于协同过滤pythondjangovue…

Python编码系列—Python原型模式:深克隆与高效复制的艺术

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

C++的IO流(文件部分在这里)

1. C语言的输入与输出 C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键盘)读取数据&#xff0c;并将值存放在变量中。printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。 注意宽度输出和精度输出控制。C语言借助了相应的缓…

嵌入式开发—CAN通信协议详解与应用(上)

文章目录 1.CAN简介CAN协议的诞生背景CAN协议的发展历程CAN协议的影响CAN通信的主要特点 2.CAN数据帧的帧格式CAN标准数据帧的帧格式CAN标准数据帧的帧格式结构图CAN扩展帧的帧格式CAN遥控帧的帧格式CAN错误帧的帧格式 3.CAN数据传输中的位填充位填充的概念位填充的作用位填充的…

今天中秋,中秋快乐,分析一个中秋月饼的项目

特色功能 使用obj模型&#xff0c;搭配tga文件&#xff0c;附加上颜色 normalMap 是让字和线条看起来更清楚和真实 高光贴图 凹凸贴图 ...... 源码 https://github.com/Lonely1201/lonely1201.github.io/tree/main/Juejin/mooncake 在线预览 https://lonely1201.githu…

将YYYY-MM-DD HH:mm:ss格式化为YYYY-MM-DD (星期一) 下午 ?点

分为凌晨、早上、中午、晚上 function formatDate(inputDate) {const date new Date(inputDate);date.setHours(date.getHours() - 1);const year date.getFullYear();const month date.getMonth() 1; // 月份从0开始const day date.getDate();let hours date.getHours(…