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

文章目录

  • 三、信号的阻塞(信号的保存)
    • 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 表中。
  • 信号处理阶段:信号在 内核态 切换回 用户态 时,才会被处理,处理方式或默认或忽略或自定义。

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

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

相关文章

安卓AndroidStdio控制台乱码解决

安卓AndroidStdio控制台乱码解决 情况&#xff1a; 在AndroidStudio中新建了一个Java Module&#xff0c;但是点击 Run ‘app’之后&#xff0c;Build Output 控制台输出的中文都是乱码&#xff0c;都是问号一样的字符 第一个解决方案 File Encodings 改为UTF-8&#xff1f; …

鸿蒙语言ArkTS(更好的生产力与性能)

ArkTS是鸿蒙生态的应用开发语言 ArkTS提供了声明式UI范式、状态管理支持等相应的能力&#xff0c;让开发者可以以更简洁、更自然的方式开发应用。 同时&#xff0c;它在保持TypeScript&#xff08;简称TS&#xff09;基本语法风格的基础上&#xff0c;进一步通过规范强化静态检…

5G——小区搜索流程

小区搜索流程 小区搜索目标&#xff1a;读取到SIB1. 小区搜索流程概述&#xff1a;SIB1在PDSCH信道承载&#xff0c;承载SIB1的信道在哪个位置由PDCCH告诉&#xff0c;而PDCCH的基本信息由MIB告诉&#xff0c;MIB信息由广播信道PBCH广播出去&#xff0c;物理信道解调需要解调…

Codeforces Round 926 (Div. 2) C. Sasha and the Casino (Java)

Codeforces Round 926 (Div. 2) CC. Sasha and the Casino (Java) 比赛链接&#xff1a;Codeforces Round 926 (Div. 2) C题传送门&#xff1a;C. Sasha and the Casino 题目&#xff1a;C. Sasha and the Casino **Example ** input 2 1 7 2 1 1 2 3 15 3 3 6 4 4 5 5 4 7…

EXCEL中不错的xlookup函数

excel中一般要经常用vlookup函数&#xff0c;但其实经常麻烦要正序&#xff0c;从左边到右边&#xff0c;还要数列&#xff0c;挺麻烦的&#xff0c;xlookup的函数还不错&#xff0c;有个不错的一套视频介绍,B站的&#xff0c;地址是&#xff1a;XLOOKUP函数基础用法&#xff0…

BIG DATA —— 大数据时代

大数据时代 [英] 维克托 迈尔 — 舍恩伯格 肯尼斯 库克耶 ◎ 著 盛杨燕 周涛◎译 《大数据时代》是国外大数据研究的先河之作&#xff0c;本书作者维克托迈尔舍恩伯格被誉为“大数据商业应用第一人”&#xff0c;他在书中前瞻性地指出&#xff0c;大数据带来的信息…

2024.2.17日总结(小程序开发)

父子组件之间的通信 父子组件之间通信的3种方式 属性绑定 用于父组件向子组件的指定属性设置数据&#xff0c;仅能设置JSON 兼容的数据属性绑定用于实现父向子传值&#xff0c;而且只能传递普通类型的数据&#xff0c;无法将方法传递给子组件 事件绑定 用于子组件向父组件…

详解自定义类型:枚举与联合体!

目录 ​编辑 一、枚举类型 1.枚举类型的声明 2.枚举类型的优点 3.枚举类型的使用 二、联合体类型(共用体&#xff09; 1.联合体类型的声明 2.联合体的特点 3.相同成员的结构体和联合体的对比 4.联合体大小的计算 5.用联合体判断大小端 三.完结散花 悟已往之不谏&…

由于找不到MSVCP140.dll无法运行软件游戏,多种解决方法分享

电脑系统在运行过程中&#xff0c;当出现“由于找不到MSVCP140.dll”这一提示时&#xff0c;可能会引发一系列潜在的问题与影响。当电脑无法找到这个特定的dll文件时&#xff0c;意味着相关应用可能无法顺利加载并执行必要的组件&#xff0c;进而导致程序无法启动或运行过程中频…

CCF编程能力等级认证GESP—C++8级—20231209

CCF编程能力等级认证GESP—C8级—20231209 单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09;判断题&#xff08;每题 2 分&#xff0c;共 20 分&#xff09;编程题 (每题 25 分&#xff0c;共 50 分)奖品分配大量的工作沟通 答案及解析单选题判断题编程题1编程题2…

HTTPS网络通信协议基础

目录 前言&#xff1a; 1.HTTPS协议理论 1.1协议概念 1.2加密 2.两类加密 2.1对称加密 2.2非对称加密 3.引入“证书” 3.1证书概念 3.2数据证书内容 3.3数据签名 4.总结 前言&#xff1a; 了解完HTTP协议后&#xff0c;HTTPS协议是HTTP协议的升级加强版&#xff0c…

基于协同过滤的时尚穿搭推荐系统

项目&#xff1a;基于协同过滤的时尚穿搭推荐系统 摘 要 基于协同过滤的时尚穿搭推荐系统是一种能自动从网络上收集信息的工具&#xff0c;可根据用户的需求定向采集特定数据信息的工具&#xff0c;本项目通过研究服饰流行的分析和预测的分析和预测信息可视化时尚穿搭推荐系统…

相机图像质量研究(32)常见问题总结:图像处理对成像的影响--振铃效应

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…

无人机应用场景和发展趋势,无人机技术的未来发展趋势分析

随着科技的不断发展&#xff0c;无人机技术也逐渐走进了人们的生活和工作中。无人机被广泛应用于很多领域&#xff0c;例如遥感、民用、军事等等。本文将围绕无人机技术的应用场景和发展趋势&#xff0c;从多角度展开分析。 无人机技术的应用场景 无人机在遥感方面的应用&…

文生视频提示词:故事与主题

内容创意 --故事与主题 Story & Theme 这些词汇覆盖了从基本的故事类型到特定的主题和元素&#xff0c;可用于激发创意和定义视频内容的核心主题。 Adventure 冒险 Romance 浪漫 Mystery 神秘 Fantasy 幻想 Science Fiction 科幻 Horror 恐怖 Thriller 惊悚 Comedy 喜剧 Dr…

Ubuntu20.04 安装jekyll

首先使根据官方文档安装&#xff1a;Jekyll on Ubuntu | Jekyll • Simple, blog-aware, static sites 如果没有报错&#xff0c;就不用再继续看下去了。 我这边在执行gem install jekyll bundler时报错&#xff0c;所以安装了rvm&#xff0c;安装rvm可以参考这篇文章Ubuntu …

阿里云服务器ECS租赁费用报价_CPU内存_带宽和系统盘价格表

2024年最新阿里云服务器租用费用优惠价格表&#xff0c;轻量2核2G3M带宽轻量服务器一年61元&#xff0c;折合5元1个月&#xff0c;新老用户同享99元一年服务器&#xff0c;2核4G5M服务器ECS优惠价199元一年&#xff0c;2核4G4M轻量服务器165元一年&#xff0c;2核4G服务器30元3…

Python学习04 --- 函数进阶以及文件相关操作

1.函数的多返回值 1.函数可以有多个返回值&#xff0c;返回值之间用逗号隔开 2.多个返回值之间的类型可以不同 3.函数返回多个返回值时&#xff0c;我们也要用多个变量接收函数返回值&#xff0c;具体语法如上&#xff1a; 根据位置一一对应&#xff0c;x,y,z分别对应返回值…

fusion360 操作总结(不断更新)

平移缩放旋转快捷键 画布选择Windows 组合键macOS 组合键平移按住鼠标中键按住鼠标中键缩放滚动鼠标中键滚动鼠标中键动态观察旋转按住 Shift 键并按住鼠标中键按住 Shift 键并按住鼠标中键绕点动态观察按住 Shift 键单击并按住鼠标中键按住 Shift 键单击并按住鼠标中键撤消Ct…

springboot197基于springboot的毕业设计系统的开发

简介 【毕设源码推荐 javaweb 项目】基于springbootvue 的毕业设计系统的开发 适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负。 看运行截图看 第五章 第四章 …