【Linux】进程信号的保存 | 自定义捕捉

news2024/10/4 10:29:37

文章目录

  • 三、信号的阻塞(信号的保存)
    • 1. 信号相关其他常见概念
    • 2. 在内核中的表示
    • 3. sigset_t类型
    • 4. 信号集操作函数
      • 函数列表
      • 注意事项
    • 5. 读取/修改block位图 - sigprocmask
    • 6. 读取pending位图 - sigpending
  • 四、信号捕捉
    • 1. 信号捕捉的初步认识
      • 自定义捕捉
      • 总结思考
    • 2. 再谈进程地址空间
      • 内核空间与用户空间
      • 用户态和内核态
    • 3. 内核如何实现信号的捕捉
    • 4. sigaction函数
  • 五、信号部分的总结


信号产生
信号保存
信号处理

信号的概念和信号如何产生已经在 【Linux】进程信号概念 | 核心转储 | 信号的产生 中介绍了,本文来介绍剩下的信号的保存(阻塞)和信号的捕捉。


三、信号的阻塞(信号的保存)

1. 信号相关其他常见概念

  • 实际执行信号的处理动作,称为信号递达(delivery)。
  • 信号从产生到递达之间的状态,称为信号未决(pending)。
  • 进程可以选择阻塞(block)某个信号,表示该进程当前不想收到这个信号,收到后把它标记为未决,但不处理它,直到解除阻塞。换句话说,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 需要注意的是,阻塞和忽略是不同的,只要信号被阻塞就暂时不会递达,而忽略是在递达之后的一种处理动作

产生一个信号,信号是未决的,这个信号不一定是阻塞的。
一个信号如果被进程阻塞,进程收到该信号之后,一定会标记它为未决。



2. 在内核中的表示

  • 信号在内核中的表示示意图如下:请添加图片描述

  • 对于每个进程的pcb(task_struct对象),其中的每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作:请添加图片描述

  • 信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

  • 在示意图的例子中:

    1. SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
    2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
    3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它解除阻塞后会执行的处理动作是用户自定义函数sighandler()
  • 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
    POSIX.1允许系统递送该信号一次或多次。
    Linux是这样实现的:

    • 常规信号在递达之前产生多次只计一次,毕竟未决只能是0或1,不可能记录次数。
    • 实时信号在递达之前产生多次可以依次放在一个队列里,本章不讨论实时信号。

用表格整理信号位图比特位的含义和现象:

信号状态比特位为1 (有效) 成因比特位为0 (有效) 成因运行现象
阻塞进程使用系统调用 sigprocmask 或类似方法设置阻塞标志位进程使用系统调用 sigprocmask 或类似方法解除阻塞标志位阻塞的信号不会被处理,直到解除阻塞
未决信号产生,且未被进程处理进程处理了一个未决信号未决的信号等待被处理,直到进程执行相应信号的处理动作

用流程图整理信号传递的过程:

cluster_final
cluster_reached
cluster_pending
cluster_blocked
cluster_signaled
未决
如果该信号被标记为阻塞
如果该信号未阻塞
解除阻塞
未决
解除未决
操作系统尽快让信号递达
忽略该信号
信号默认处理动作
信号的自定义捕捉
结束
结束
结束
继续运行/终止进程
执行信号处理动作
进程状态更新 - 忽略
进程状态更新 - 默认处理
进程状态更新 - 自定义处理
处理未决信号
清除未决标志
信号被阻塞
信号未决标志仍存在
设置未决标志
信号产生
cluster_unblock

3. sigset_t类型

上文提到,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

未决和阻塞的标志可以用相同的数据类型sigset_t来存储:

typedef unsigned long sigset_t;

sigset_t就是信号位图,这个类型的每个比特位可以表示对应信号的“有效”或“无效”状态:

  • 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞。
  • 在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)这里的“屏蔽”应该理解为阻塞而不是忽略

未决标志是在信号产生时,如果该信号没有被阻塞,并且进程没有设置信号的忽略处理方式,内核会将相应信号的未决比特位置1。

阻塞是进程的主动行为,可以通过系统调用(如sigprocmask)来设置阻塞,将特定信号的阻塞比特位置1。



4. 信号集操作函数

在Linux中,sigset_t 类型用一个 bit 表示每种信号的“有效”或“无效”状态。使用者不需要关心该类型内部的数据存储方式,只能通过以下函数来操作 sigset_t 变量,而不应直接解释其内部数据。使用 printf 直接打印 sigset_t 变量是没有意义的。

函数列表

<signal.h> 中的系统调用描述
int sigemptyset(sigset_t *set);初始化 set 所指向的信号集,将其中所有信号的对应 bit 清零,表示该信号集不包含任何有效信号。
int sigfillset(sigset_t *set);初始化 set 所指向的信号集,将其中所有信号的对应 bit 置位,表示该信号集的有效信号包括系统支持的所有信号。
int sigaddset(sigset_t *set, int signo);set 所指向的信号集中添加某个有效信号。
int sigdelset(sigset_t *set, int signo);set 所指向的信号集中删除某个有效信号。
int sigismember(const sigset_t *set, int signo);判断一个信号集的有效信号中是否包含某个信号。若包含则返回 1,不包含则返回 0,出错返回 -1。

使用一下上面的函数:

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

int main()
{
	sigset_t s; // 用户空间定义的变量

	sigemptyset(&s); // 初始化 `set` 所指向的信号集,将其中所有信号的对应 bit 清零,表示该信号集不包含任何有效信号

	sigfillset(&s); // 初始化 `set` 所指向的信号集,将其中所有信号的对应 bit 置位,表示该信号集的有效信号包括系统支持的所有信号

	sigaddset(&s, SIGINT); // 向 `set` 所指向的信号集中添加某个有效信号

	sigdelset(&s, SIGINT); // 从 `set` 所指向的信号集中删除某个有效信号

	bool test = sigismember(&s, SIGINT); // 判断一个信号集的有效信号中是否包含某个信号。若包含则返回 1,不包含则返回 0,出错返回 -1
	if (test)
		printf("true\n");
	else
		printf("false\n");
	return 0;
}

结果:false

注意事项

在使用 sigset_t 类型的变量之前,必须调用 sigemptysetsigfillset 进行初始化,以确保信号集处于确定的状态。初始化后,可以调用 sigaddsetsigdelset 在该信号集中添加或删除某个有效信号。

这四个函数都是成功返回 0,出错返回 -1。sigismember 是一个布尔函数,用于判断一个信号集的有效信号中是否包含某个信号,若包含则返回 1,不包含则返回 0,出错返回 -1。



5. 读取/修改block位图 - sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

返回值:若成功则为0,若出错则为-1

使用一下:

#include <iostream>
#include <unistd.h>
#include <signal.h>

signed main()
{
	std::cout << "getpid: " << getpid() << std::endl;
	sigset_t block, oblock;

	sigemptyset(&block);
	sigemptyset(&oblock);

	for (int signo = 1; signo <= 31; signo++) // 把1-31信号全写入block
	{
		sigaddset(&block, signo); // 在这里只是修改了block变量,没有让OS真正屏蔽signo号信号;
	}

	sigprocmask(SIG_BLOCK, &block, &oblock); // 将该进程的block位图替换成我们的block位图

	while (true)
	{
		std::cout << "我已经屏蔽了所有的信号,来打我呀!" << std::endl;
		sleep(1);
	}
}

使用下面的bash脚本,在另外一个bash下每隔一秒kill一下该进程:

i=1; while :; do echo "send signal:${i}..."; kill -${i} 7422; sleep 1; let i++; done

发现除了9号和19号信号,其他信号都能成功屏蔽:
请添加图片描述

请添加图片描述

印证了之前说的SIGKILL (9号信号)SIGSTOP (19号信号)不能被捕获、阻止或忽略。



6. 读取pending位图 - sigpending

sigpending函数可以用于读取进程的未决信号集,该函数的函数原型如下:

int sigpending(sigset_t *set);

sigpending函数读取当前进程的未决信号集,通过set参数传出。该函数调用成功返回0,出错返回-1。

实验一下:

  1. 先用上述的函数将2号信号进行屏蔽(阻塞)。
  2. 使用kill命令或组合按键向进程发送2号信号。
  3. 此时2号信号会一直被阻塞,并一直处于pending(未决)状态。
  4. 使用sigpending函数获取当前进程的pending信号集进行验证。

代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>

void PrintPending(const sigset_t& pending)
{
	for (int signo = 1; signo <= 32; signo++)
	{
		if (sigismember(&pending, signo))
		{
			std::cout << "1";
		}
		else
		{
			std::cout << "0";
		}
	}
	std::cout << "\n";
}

signed main()
{
	sigset_t set, oset;
	sigemptyset(&set);
	sigemptyset(&oset);

	//1. 阻塞2号信号
	sigaddset(&set, 2); //SIGINT
	sigprocmask(SIG_SETMASK, &set, &oset);

	//2. 让进程不断获取当前自己的pending位图
	sigset_t pending;
	sigemptyset(&pending);
	while (1)
	{
		sigpending(&pending); //获取pending
		PrintPending(pending); //打印pending位图(1表示未决)
		sleep(1);
	}
	return 0;
}

可以看到,程序刚刚运行时,因为没有收到任何信号,所以此时该进程的pending表一直是全0,而当我们使用kill命令向该进程发送2号信号后,由于2号信号是阻塞的,进程收到但不处理2号信号,因此2号信号一直处于未决状态,所以我们看到pending表中的第二个数字一直是1,且进程不会退出:请添加图片描述

问题:一个信号被递达,pending位图会将该信号的标志位从1改成0,这个修改发生在执行递达动作前还是递达动作执行完成后?


我们可以让进程自定义捕捉2号信号,让handler函数打印pending位图,就可以验证修改pending位图的动作的发生时机:

#include \<iostream>
#include <unistd.h>
#include <signal.h>

void PrintPending(const sigset_t& pending)
{
	std::cout << "\n";
	for (int signo = 1; signo <= 32; signo++)
	{
		if (sigismember(&pending, signo))
		{
			std::cout << "1";
		}
		else
		{
			std::cout << "0";
		}
	}
	std::cout << "\n";
}

void handler(int signo)
{
	sigset_t pending;
	sigpending(&pending);
	PrintPending(pending);
	std::cout << "handler of signal " << signo << std::endl;
}

signed main()
{
	signal(2, handler);
	
	while (true)
	{
		sleep(1);
	}
	return 0;
}

请添加图片描述

实际上,键盘按下 Ctrl+C 之后,向当前进程发送2号信号,信号执行自定义的递达动作handler之前,OS已经将pending位图由1置0了。



四、信号捕捉

1. 信号捕捉的初步认识

自定义捕捉

实际上当用户按Ctrl+C时,这个键盘输入会产生一个硬件中断,被操作系统获取并解释成信号(Ctrl+C被解释成2号信号),然后操作系统将2号信号发送给目标前台进程,当前台进程收到2号信号后就会退出。

我们可以使用signal函数对2号信号进行捕捉,证明当我们按Ctrl+C时进程确实是收到了2号信号。使用signal函数时,我们需要传入两个参数,第一个是需要捕捉的信号编号,第二个是对捕捉信号的处理方法,该处理方法的参数是int,返回值是一个函数指针,指向原来的信号处理函数:

   #include <signal.h>

   typedef void (*sighandler_t)(int);

   sighandler_t signal(int signum, sighandler_t handler);

例如,下面的代码中将2号信号进行了捕捉,当该进程运行起来后,若该进程收到了2号信号就会打印出收到信号的信号编号。

#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
	printf("catch a sig : %d\n", sig);
}
int main()
{
	signal(2, handler); //信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的,提前了解一下
	while(1);
	return 0;
}

请添加图片描述



总结思考

  1. 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
    因为OS是进程的管理者。

  2. 信号的处理是否是立即处理的?
    不是所有信号的处理都是立即进行的,而是在合适的时候处理,“合适的时候”是指进程从内核态返回到用户态的时候。有些信号,例如 SIGKILL,会立即终止进程。但对于其他信号,处理可能会延迟,具体取决于进程的状态以及是否被阻塞。

  3. 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
    是的,如果信号不能立即处理,需要被暂时记录下来。这通常在进程的 pending 结构体中进行记录,其中包括一个位图用于表示未决信号的状态。

  4. 一个进程在没有收到信号的时候,能否知道自己应该对合法信号作何处理呢?
    进程可以在收到信号之前,通过注册信号处理函数来定义对合法信号的处理方式。这通常通过使用 signal()sigaction() 系统调用来实现。进程可以指定默认的处理动作,或者自定义处理函数。

  5. 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
    当发生触发信号的事件时(例如按下 Ctrl+C 产生SIGINT),操作系统会向相应进程发送信号。该信号会被记录在进程的 pending 位图中。如果信号不被阻塞,进程会根据信号的处理方式(默认动作、自定义处理函数等)来执行相应的处理。如果信号被阻塞,信号会在解除阻塞后递达,然后按照相应的处理方式进行处理。



2. 再谈进程地址空间

内核空间与用户空间

每一个进程都有自己的进程地址空间,该进程地址空间由内核空间用户空间组成:请添加图片描述

  • 用户所写的代码和数据位于用户空间,通过用户级页表与物理内存之间建立映射关系。
  • 内核空间存储的实际上是操作系统代码和数据,通过内核级页表与物理内存之间建立映射关系。内核级页表是一个全局的页表,它用来维护操作系统的代码与进程之间的关系。因此:
    • 在每个进程的进程地址空间中,用户空间是属于当前进程的,每个进程看到的代码和数据是完全不同的,只能看到自己的那一份;
    • 内核空间所存放的都是操作系统的代码和数据,所有进程看到的都是一样的内容,OS的代码和数据被所有内存共享。

虽然每个进程都能够看到操作系统,但并不意味着每个进程都能够随时对其进行访问,当进程访问用户空间时进程必须处于用户态,当进程访问内核空间时进程必须处于内核态。



用户态和内核态

用户态:执行用户所写的代码时,就属于 用户态
内核态:执行操作系统的代码时,就属于 内核态

从用户态切换为内核态通常有如下几种情况:

  1. 需要进行系统调用时。
  2. 当前进程的时间片到了,导致进程切换。
  3. 产生异常、中断、陷阱等。

与之相对应,从内核态切换为用户态有如下几种情况:

  1. 系统调用返回时。
  2. 进程切换完毕。
  3. 异常、中断、陷阱等处理完毕。

其中,由用户态切换为内核态我们称之为陷入内核。每当我们需要陷入内核的时,本质上是因为我们需要执行操作系统的代码,比如系统调用函数是由操作系统实现的,我们要进行系统调用就必须先由用户态切换为内核态。

陷入内核和切换回用户态的底层实现:

CPU 中,存在一个 CR3 寄存器,这个 寄存器 的作用就是用来表征当前处于 用户态 还是 内核态

  • 当寄存器中的值为 3 时:表示正在执行用户的代码,也就是处于用户态
  • 当寄存器中的值为 0 时:表示正在执行操作系统的代码,也就是处于 内核态

请添加图片描述



3. 内核如何实现信号的捕捉

当我们在执行主控制流程的时候,可能因为某些情况而陷入内核,当内核处理完毕准备返回用户态时,一定会进行信号pending表的检查。(此时仍处于内核态,有权力查看当前进程的pending位图)

在查看pending位图时,如果发现有未决信号,并且该信号没有被阻塞,那么此时就需要该信号进行处理,因为处理方式有默认和自定义两种情况,我们分情况讨论一下:


  • 情况1:信号的默认处理(如果没有用signal()sigaction()来注册信号的自定义捕捉行为)


    如果待处理信号的处理动作是默认或者忽略,则执行该信号的处理动作后清除对应的pending标志位,如果没有新的信号要递达,就直接返回用户态,从主控制流程中上次被中断的地方继续向下执行即可。请添加图片描述

  • 情况2:自定义捕捉(用户代码中用signal()sigaction()注册了信号的自定义捕捉行为)


    如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这就是信号的捕捉。用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了请添加图片描述

上面两次从内核态回到用户态的过程中,都会检查pending表,因为要检查有没有需要处理的信号。

简化一下上面这张图:请添加图片描述



4. sigaction函数

捕捉信号除了用前面用过的signal()函数之外,我们还可以使用sigaction()函数对信号进行捕捉,sigactionsignal 功能更丰富,sigaction()函数的函数原型如下:

NAME
       sigaction - examine and change a signal action
       
SYNOPSIS
       #include <signal.h>
       int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

sigaction结构体:

struct sigaction 
{
	void     (*sa_handler)(int);	                    //自定义动作
	void     (*sa_sigaction)(int, siginfo_t *, void *);	//实时信号相关
	sigset_t   sa_mask;	                                //待屏蔽的信号集
	int        sa_flags;	                            //一些选项,一般设为 0
	void     (*sa_restorer)(void);	                    //实时信号相关
};

返回值:成功返回 0,失败返回 -1 并将错误码设置
参数1:待操作的信号
参数2:sigaction 结构体,具体成员如上所示
参数3:保存修改前进程的 sigaction 结构体信息

我们可以像使用signal()一样使用sigaction()

#include <iostream>
#include <signal.h>
using namespace std;

void PrintPending(const sigset_t& pending)
{
	std::cout << " 当前进程的 pending 表为: ";
	for (int signo = 1; signo <= 32; signo++)
	{
		if (sigismember(&pending, signo))
		{
			std::cout << "1";
		}
		else
		{
			std::cout << "0";
		}
	}
	std::cout << "\n";
}

void handler(int signo)
{
	sigset_t pending;
	sigpending(&pending);
	while (true)
	{
		PrintPending(pending);
		sleep(2);
	}
}

int main()
{
	cout << "当前进程的pid:" << getpid() << endl;
	struct sigaction act, oact;
	// 初始化自定义动作
	act.sa_handler = handler;

	// 给2号信号注册自定义动作
	sigaction(2, &act, &oact);

	while (true);

	return 0;
}

只对二号信号进行了自定义捕捉,收到二号信号后不断打印pending位图,此时发别的信号依然能正常终止该进程,运行现象和用signal()一样:
请添加图片描述

但是如果设置了sa_mask字段,则当进程递达信号并执行用户自定义动作handler时,可以将部分信号进行屏蔽直到用户自定义动作执行完成

加入对3,4,5信号的屏蔽:

//初始化 屏蔽信号集 
sigaddset(&act.sa_mask, 3); 
sigaddset(&act.sa_mask, 4); 
sigaddset(&act.sa_mask, 5);

完整代码:

#include <iostream>
#include <signal.h>
#include <assert.h>
using namespace std;

void PrintPending(const sigset_t& pending)
{
	std::cout << " 当前进程的 pending 表为: ";
	for (int signo = 1; signo <= 32; signo++)
	{
		if (sigismember(&pending, signo))
		{
			std::cout << "1";
		}
		else
		{
			std::cout << "0";
		}
	}
	std::cout << "\n";
}

void handler(int signo)
{
	cout << signo << "号信号递达了" << endl;
	sigset_t pending;

	int cnt = 15;
	while (cnt--)
	{
		int ret = sigpending(&pending);
		assert(ret == 0);
		(void)ret; // 假装用一下ret,欺骗编译器,避免 release 模式中出错

		PrintPending(pending);
		sleep(1);
	}
}

int main()
{
	cout << "当前进程的pid:" << getpid() << endl;
	struct sigaction act, oact;
	// 初始化 自定义动作
	act.sa_handler = handler;

	//初始化 屏蔽信号集 
	sigaddset(&act.sa_mask, 3);
	sigaddset(&act.sa_mask, 4);
	sigaddset(&act.sa_mask, 5);

	// 给2号信号注册自定义动作
	sigaction(2, &act, &oact);

	while (true);

	return 0;
}

2 号信号的循环结束(10 秒),3、4、5 信号的 阻塞 状态解除,立即被 递达,进程就被干掉了:请添加图片描述



五、信号部分的总结

信号的知识按照:

信号产生
信号保存
信号处理

的顺序展开:

  • 信号产生阶段:有四种产生方式,包括 键盘键入、系统调用、软件条件、硬件异常。
  • 信号保存阶段:内核中存在三张表,blcok 表、pending 表以及 handler 表,信号在产生之后,存储在 pending 表中。
  • 信号处理阶段:信号在 内核态 切换回 用户态 时,才会被处理,处理方式或默认或忽略或自定义。

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

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

相关文章

Stable Diffusion 绘画入门教程(webui)-提示词

通过上一篇文章大家应该已经掌握了sd的使用流程&#xff0c;本篇文章重点讲一下提示词应该如何写 AI绘画本身就是通过我们写一些提示词&#xff0c;然后生成对应的画面&#xff0c;所以提示词的重要性不言而喻。 要想生成更加符合自己脑海里画面的图片&#xff0c;就尽量按照…

孢子捕捉仪结构

TH-BZ2在现代农业科技的浪潮中&#xff0c;孢子捕捉仪作为一种先进的农业监测设备&#xff0c;正逐渐走进人们的视野。它以其独特的结构和功能&#xff0c;为农作物的健康生长提供了有力保障。那么&#xff0c;孢子捕捉仪究竟是何方神圣&#xff1f;它的结构又是怎样的呢&#…

vulnhub靶场之doubletrouble

一.环境搭建 1.靶场描述 get flags difficulty: easy about vm: tested and exported from virtualbox. dhcp and nested vtx/amdv enabled. you can contact me by email for troubleshooting or questions. This works better with VirtualBox rather than VMware 2.下载地…

详解4大C语言内存函数【超详细建议点赞收藏】

目录 1. memcpy----内存拷贝1.1 函数介绍1.2 函数使用1.3 模拟实现 2. memmove----重叠内存的数据拷贝2.1 函数介绍2.2 函数使用2.3 模拟实现 3. memcmp----内存比较3.1 函数介绍3.2 函数使用 4.memset----内存设置4.1 函数介绍4.2 函数使用 注意&#xff1a;以下4个内存函数在…

VFH特征的使用(二)

一、VFH特征识别 C recognize_objects.h #pragma once #include <pcl/point_cloud.h> #include <pcl/point_types.h> #include <pcl/common/common.h> #include <pcl/common/transforms.h> #include <pcl/console/parse.h> #include <pcl/…

基于Spring Boot的智能物流管理系统,计算机毕业设计(带源码+论文)

源码获取地址&#xff1a; 码呢-一个专注于技术分享的博客平台一个专注于技术分享的博客平台,大家以共同学习,乐于分享,拥抱开源的价值观进行学习交流http://www.xmbiao.cn/resource-details/1759581137025445890

做temu跨境电商,必读这五大秘诀!

随着互联网的飞速发展&#xff0c;电商行业呈现出前所未有的繁荣景象。新兴电商平台Temu成为了众多创业者的关注焦点。本文将为您解析如何在Temu电商蓝海项目中&#xff0c;从自身能力建设、了解市场环境到做好定位等方面&#xff0c;找到属于您的成功之路。 一、自身能力建设 …

【白嫖8k买的机构vip教程】Selenium(3):python+selenium环境安装

准备工具如下&#xff1a; Python安装包&#xff1a;https://www.python.org/getit/PyCharm Pycharm安装包&#xff1a;http://www.jetbrains.com/pycharm/download/Selenium Selenium安装包&#xff1a;https://pypi.python.org/pypi/selenium、或者在pycharm中直接下载seleni…

Vector - CANoe - CAPL重启VN设备退出BusOff

在总线测试中进行BusOff测试的时候&#xff0c;偶尔会遇到将Vector工具链下的VN系列设备进入到BusOff状态&#xff0c;这个时候我们就只能重启CANoe才能将VN系列设备进行重启&#xff0c;才可以再次在Trace窗口上看到发送和接收的报文。不过在某些特定的情况的下&#xff0c;我…

【大模型 知识图谱】ChatKBQA:KBQA知识图谱问答 + 大模型

ChatKBQA&#xff1a;KBQA知识图谱问答 大模型 提出背景传统方法处理流程ChatKBQA处理流程对比优势 总结ChatKBQA框架概览特征1&#xff1a;逻辑形式生成特征2&#xff1a;无监督实体和关系检索特征3&#xff1a;参数高效的微调特征4&#xff1a;GQoT 可解释的查询执行特征5&a…

【白嫖8k买的机构vip教程】安卓设备连接电脑进行adb命令操作

Android 真机进行adb 命令的操作&#xff1a; 连接方式 &#xff1a; 数据线连接&#xff1a; 1&#xff09;手机需通过数据线连接电脑&#xff1b; 2&#xff09;手机调出开发者选项&#xff1b;手机操作步骤&#xff1a;设置——系统——关于手机&#xff08;平板电脑&am…

VMware还原Windows11 ghost镜像

文章目录 环境步骤准备制作启动iso文件创建虚拟机启动虚拟机还原Windows 参考 环境 Windows 11 家庭中文版VMware Workstation 17 Pro石大师装机大师Windows 11 ghost系统镜像 步骤 准备 下载好Windows 11 ghost系统镜像&#xff0c;我下载的文件是 FQ_WIN11_X64_VDL_V2080…

【教3妹学编程-算法题】将数组分成最小总代价的子数组 II

2哥 : 叮铃铃&#xff0c;3妹&#xff0c;过年干嘛呢&#xff0c;是不是逛吃逛吃&#xff0c;有没有长胖呢。 3妹&#xff1a;切&#xff0c;我妈张罗着要给我相亲呢。 2哥 : 相亲&#xff1f;哈哈哈哈 3妹&#xff1a;别笑了&#xff0c;我妈说跟我年龄相等的人都已经孩子上小…

为什么2023年是AI视频的突破年,以及对2024年的预期#a16z

2023年所暴露的AI生成视频的各种问题&#xff0c;大部分被OpenAI发布的Sora解决了吗&#xff1f;以下为a16z发布的总结&#xff0c;在关键之处&#xff0c;我做了OpenAI Sora的对照备注。 推荐阅读&#xff0c;了解视频生成技术进展。 Why 2023 Was AI Video’s Breakout Year,…

开发消息多发工具需要用到的源代码

在数字化时代&#xff0c;消息传递是许多应用程序的核心功能之一&#xff0c;从社交媒体到企业通信&#xff0c;从个人聊天到群发消息&#xff0c;消息传递无处不在&#xff0c;为了满足这种需求&#xff0c;开发者经常需要创建或定制消息多发工具。 这些工具通常需要处理多个…

MySQL数据库基础(十):DQL数据查询语言

文章目录 DQL数据查询语言 一、数据集准备 二、select查询 三、简单查询 四、条件查询 1、比较查询 2、范围查询 3、逻辑查询 4、模糊查询 5、非空查询 五、排序查询 六、聚合查询 七、分组查询与having子句 1、分组查询介绍 2、group by的使用 3、group by 聚…

【深入理解设计模式】单例设计模式

单例设计模式 概念&#xff1a; 单例模式&#xff08;Singleton Pattern&#xff09;是 Java 中最简单的设计模式之一。 单例设计模式是一种创建型设计模式&#xff0c;其主要目的是确保类在应用程序中的一个实例只有一个。这意味着无论在应用程序的哪个位置请求该类的实例&a…

网站常见的反爬手段及反反爬思路

摘要:介绍常见的反爬手段和反反爬思路&#xff0c;内容详细具体&#xff0c;明晰解释每一步&#xff0c;非常适合小白和初学者学习&#xff01;&#xff01;&#xff01; 目录 一、明确几个概念 二、常见的反爬手段及反反爬思路 1、检测user-agent 2、ip 访问频率的限制 …

Web服务器基础

Web服务器基础 【一】前端概述 【1】HTML HTML&#xff08;超文本标记语言&#xff09;是用于创建网页结构的标记语言。它定义了网页的骨架&#xff0c;包括标题、段落、列表、链接等元素&#xff0c;但没有样式。可以将HTML视为网页的结构和内容的描述。 【2】CSS css&…

NAS系统折腾记 | TinyMediaManager刮削电影海报

搭建好了NAS系统和Emby Media Server&#xff0c;接下来就是怎样对下载好的电影/电视剧集等内容进行刮削来展示电影海报墙获得更好的效果了。实际上&#xff0c;Emby Server本身就内置了强大的元数据抓取功能&#xff0c;能够自动从互联网上抓取电影、电视剧的元数据和海报等信…