【Linux】信号概念与信号产生

信号概念与信号产生

  • 一、初识信号
    • 1. 信号概念
    • 2. 前台进程和后台进程
    • 3. 认识信号
    • 4. 技术应用角度的信号
  • 二、信号的产生
    • 1. 键盘组合键
    • 2. kill 命令
    • 3. 系统调用
    • 4. 异常
      • (1)观察现象
      • (2)理解本质
    • 5. 软件条件
      • 闹钟

一、初识信号

1. 信号概念

生活中类似信号的概念也不少,例如上课铃声响,就是信号的发出,我们听到上课铃声,就是接收到信号,我们快速回到教室上课就是对信号做出处理。那么我们是怎么认识这些信号的呢?那必定是有人教我们,然后我们记住了。而且我们不单单要认识信号,还要识别信号,知道信号的处理方法!

当信号产生了,我们可能并不立即处理这个信号,我们可能会在合适的时候再去处理,因为我们可能还有更重要的事情要做,所以在信号产生之后,必定有一个时间窗口,在这个时间窗口内,我们必须记住信号的到来!

其实在计算机中,上面中的“我们”其实就是进程!所以进程必须识别并处理信号,并且信号没有产生,也要具备处理信号的能力!所以信号的处理能力,属于进程内置功能的一部分!也就是说,当进程收到了一个信号,进程也可能并不会立即处理这个信号,在合适的时候才会处理。所以,一个进程必须当信号产生,到信号开始被处理,也一定会有时间窗口,也就是说,进程具有临时保存哪些信号已经发生了的能力。

而信号的处理方式有三种:默认动作、忽略、自定义动作;其中我们把自定义动作称为信号的捕捉。

2. 前台进程和后台进程

我们先写一个死循环,如下:

				int main()
				{
				    while(true)
				    {
				        cout << "i am a process, pid: " << getpid() << endl;
				        sleep(1);
				    }
				    return 0;
				}

当我们运行起来之后,我们可以通过 ctrl + c 杀掉这个进程。而这种进程,当我们运行起来之后,我们的 bash 就不能接收任何指令了,我们把这种进程称为前台进程

我们也可以在运行该程序的时候,在后面加上 &,此时我们运行程序,我们可以输入指令,bash 可以接收我们的指令,也就是说我们还能正常使用 bash 命令行,但是此时我们使用 ctrl + c 就杀不掉该进程了,这种进程我们称为后台进程,如下图:

在这里插入图片描述

Linux中,一次登录中,一个终端一般会配上一个 bash,每一个登录,只允许一个进程是前台进程,但是可以允许多个进程是后台进程。所以我们运行一个程序的时候,默认是在前台运行的,此时 bash 进程就变成后台进程了,所以此时我们运行指令是没有用的。所以前台进程后台进程的区别在于谁来获取键盘输入!

那么我们在运行后台进程的时候,bash 依旧是前台进程,我们输入指令的时候,从上面的结果中我们可以看到,指令已经和打印的内容混合在一起了,此时为什么还能运行我们的指令呢?其实我们输入 ls 的时候,我们是通过键盘输入的,我们键盘输入的消息,会给我们回显出来,虽然回显出来是乱的,但键盘里输入的时候输入依旧是 ls;键盘有键盘的缓冲区,显示器有显示器的缓冲区,只是我们在输入的时候默认给显示器也拷贝了一份,但是这个并不重要,重要的是输入的内容被显示器拿到就行了。

3. 认识信号

实际上,ctrl + c 本质上是被进程解释成为收到了 2号 信号,然后进程就被中断了。我们可以查看Linux中的信号列表,指令为:

				kill -l

在这里插入图片描述

其中我们发现,0号、32号和33号信号是没有的。也就是一共有62个信号;其中我们把 1~31 号信号称为普通信号;往后的称为实时信号,当信号产生必须立即处理就是实时信号;其中我们只学习普通信号。

其实信号本质上就是一个数字,我们看到上面的信号编号中,旁边的大写单词就是它的宏!

那么我们知道,进程收到2号信号的默认动作,就是终止自己。我们可以验证一下,此时我们需要认识一个接口:signal()

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

signal() 就是设置对特定一个进程的自定义处理方法;其中 typedef void (*sighandler_t)(int); 函数指针类型,返回值是空,参数是 int 的类型;那么第一个参数就是信号的编号;handler 就是对当前进程进行 signum 号信号的自定义捕捉。也就是说,当进程收到2号信号,我们就可以指定进程进行我们自定义的方法!如下代码:

				void myhandler(int signum)
				{
				    cout << "process get a signal: " << signum << endl;
				}
				
				int main()
				{
				    signal(2, myhandler);
				    while(true)
				    {
				        cout << "i am a process, pid: " << getpid() << endl;
				        sleep(1);
				    }
				    return 0;
				}

结果如下:

在这里插入图片描述

所以我们知道了,ctrl + c 的本质就是向进程发送2号信号!并且进程的默认动作已经被我们捕捉了,执行的是我们的自定义的方法。此时我们使用 ctrl + c 杀不掉进程了,所以我们可以使用 kill -9 pid 杀掉。我们也可以在我们自定义方法中加入 exit() 函数,直接退出。

注意,signal() 方法我们只需要设置一次,在该进程生命周期中,往后都有效。在我们的自定义方法中,为什么还要在参数加上信号的编号呢?因为我们可以将所有信号都设置为同一个方法,此时该方法就需要分辨是哪个信号了,所以需要加上信号的编号。

前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步的!

4. 技术应用角度的信号

接下来我们了解一下,键盘数据是如何输入给内核的,ctrl + c 又是如何变成信号的。

首先我们需要知道,键盘被按下,肯定是操作系统先知道,因为键盘设备并不能被进程直接访问。那么操作系统怎么知道键盘上有数据了呢?最简单的方法是,操作系统定期去检查键盘上的数据,因为键盘也是文件,所以在操作系统内部会有键盘的描述符、缓冲区等等,所以键盘读取的本质就是将用户层的缓冲区拷贝到内存级的缓冲区中,这就是输入的过程,如下:

在这里插入图片描述

但是操作系统怎么知道键盘上有数据了呢?同理,操作系统怎么知道我们的外设就绪了呢?例如操作系统怎么知道我们的话筒有声音呢?怎么知道摄像头有数据了呢?

曾经我们学过,CPU是不和外设打交道的,因为冯诺依曼体系,但是在控制层面上,CPU是要读取外设的!

首先,CPU 上有许多的引脚集成在主板上,而外设各种设备也是插在主板上的,而键盘在物理上其实是可以间接地和CPU相连的,CPU虽然不在键盘中读数据,但是键盘是可以在硬件上给CPU发送一个硬件中断的!

也就是说,操作系统该干嘛就干嘛,一旦键盘上有数据了,就会通过一些硬件单元将键盘当中的信息发送给 CPU;那么当有其它外设给 CPU 发送中断的时候,CPU怎么知道是哪种设备呢?所以每一种中断都有一个中断号的概念,类似于数字,我们下面假设键盘的中断号为iptnum;其实就是给 CPU 的引脚发送高低电平,由CPU来解释这个中断号是几,所以CPU就得记录下来对应外设的中断号;而在操作系统内,会有一张中断向量表,其实就是一个数组,而中断向量表中都是方法的地址,主要是包括直接访问外设的方法,包括磁盘、键盘、显示器等。所以当CPU 收到了键盘的中断号,操作系统就立马识别到CPU收到了中断号,所以操作系统会立马以中断号为索引,去中断向量表中找对应的方法,然后执行该方法,然而这个方法就是将数据从外设中拷贝到内存级缓冲区的方法!如下图:

在这里插入图片描述

其实我们学习的信号,就是用软件的方式,对进程模拟的硬件中断。

那么如果我们输入的组合键呢?操作系统怎么对 ctrl + c 这样的组合键的数据拷贝到内存级缓冲区呢?其实键盘上的按键是有分类的,有的是用来输入的,有的是用来控制的,比如 ctrl + c、z等,所以操作系统在拷贝数据的时候会进行判断,输入的是数据还是控制,如果是控制,会转化为相应的信号发送给进程!

二、信号的产生

1. 键盘组合键

上面我们已经知道了,我们可以通过 ctrl + c 这样的键盘组合键产生信号。

除了 ctrl + c 外还有 ctrl + \ ,其中 ctrl + \ 就是发送3号信号;我们将3号信号捕捉,如下:

在这里插入图片描述

还有 ctrl + z 可以发送19号信号,让对应的进程暂停。如下:

在这里插入图片描述

如果我们把19号信号捕捉呢?如下:

在这里插入图片描述

如上图,它没有捕捉到19号信号;所以我们得出,不是所有的信号都是可以被 signal 捕捉的。

所以键盘组合键发送信号常见的组合就是以下三个:

  • ctrl + c
  • ctrl + \
  • ctrl + z

我们从上面知道,不是所有的信号都是可以被 signal 捕捉的,所以我们可以尝试将所有信号都捕捉一下,然后使用 kill 指令尝试使用所有的信号编号,观察哪个信号不可以被捕捉;如下代码:

				void myhandler(int signum)
				{
				    cout << "process get a signal: " << signum << endl;
				}
				
				int main()
				{
				    for(int i = 1; i <= 31; i++)
				    {
				        signal(i, myhandler);
				    }
				
				    while(true)
				    {
				        cout << "i am a process, pid: " << getpid() << endl;
				        sleep(1);
				    }
				    return 0;
				}

最终经过实验我们得出在 1~31 号信号中只有 9号19号 信号不能被捕捉,其它都可以。

2. kill 命令

kill 命令我们就不用介绍了,直接在命令行使用即可,使用格式如下,其中 signo 为信号编号,pid 为进程的 pid

				kill -signo pid

3. 系统调用

接下来我们认识两个可以产生信号的系统调用接口,kill()raise().

  • kill()

      			int kill(pid_t pid, int sig);
    

在这里插入图片描述

其中 pid 为进程的 pidsig 为信号编号。既然有了系统接口,那么我们也可以自己实现一个 kill 指令!参考代码如下:

				int main(int argc, char* argv[])
				{
				    if(argc != 3) exit(1);
				
				    int sig = stoi(argv[1]);
				    pid_t pid = stoi(argv[2]);
				
				    int n = kill(pid, sig);
				    if(n < 0) exit(2);
				    
				    return 0;
				}
  • raise()

      			int raise(int sig);
    

在这里插入图片描述

raise() 是经过封装的;参数就是信号编号;该函数就是哪个进程调用,就将指定信号发送给哪个进程。所以 raise() 函数相当于 kill(getpid(), sig);

  • abort()

abort() 就是引起一个正常的进程直接终止。abort() 也是经过封装,其实它就是给调用进程发送 6 号信号。但是它内部做了处理,当我们捕捉了 6 号信号,但是调用了 abort() 后,虽然也调用了我们自定义方法,但是它还是会终止进程。

4. 异常

(1)观察现象

异常在我们的程序中也很常见,我们常见的异常有除0错误和越界访问,接下来我们模拟一下这两种场景,分析一下这两种场景。

  • 除0错误

      			int main()
      			{
      			    int a = 2, b = 0;
      			    cout << "...before " << endl;
      			    sleep(2);
      			    a /= b;
      			    sleep(2);
      			    cout << "...after " << endl;
      			
      			    return 0;
      			}
    

运行起来后结果如下:

在这里插入图片描述

其实是异常后,进程收到了操作系统发送的信号,终止了进程。我们看一看一下信号表,其实进程是收到了 8 号信号:

在这里插入图片描述

我们也可以使用 man 7 signal 查看信号的详细信息,如下我们找到8号信号,看到它确实是除0错误:

在这里插入图片描述

我们还可以将该信号捕捉进行验证:

				void headler(int signo)
				{
				    cout << "... get a signal: " << signo << endl;
				    sleep(1);
				}
				
				int main()
				{
				    signal(8, headler);
				    
				    int a = 2, b = 0;
				    cout << "...before " << endl;
				    a /= b;
				    cout << "...after " << endl;
				
				    return 0;
				}

结果如下:

在这里插入图片描述

如上,默认的信号处理被我们捕捉后就调用了我们的方法,而且进程不退出了,更重要的是,我们的自定义方法被一直调用,也就是说,信号一直在被触发,这是为什么呢?我们下面再解释。

  • 越界访问(野指针)

我们下面模拟一下越界访问的场景:

				int main()
				{
				    signal(8, headler);
				
				    const char* str;
				    cout << "...before " << endl;
				    cout << str[10] << endl;
				    cout << "...after " << endl;
				
				    return 0;
				}

结果如下:

在这里插入图片描述

如上图,其实是进程接受到了11号信号而被终止,如下:

在这里插入图片描述

我们也对11号信号捕捉一下,结果如下:

在这里插入图片描述

如上,进程依旧也没有退出。所以进程一旦出异常了,不一定会退出,但是一旦异常退出了,一定是执行了信号所对应的异常处理方法。

(2)理解本质

下面我们进一步理解为什么除0错误和野指针会让进程崩溃。本质上是出现异常后,给对应的进程发信号了,而进程收到信号默认的处理动作就是终止自己,这就是进程崩溃的原因。那么为什么除0错误和野指针会给进程发信号呢?那么根据我们的理解,一定是操作系统识别到了异常问题,然后给进程发信号,那么操作系统是怎么检测到异常问题的呢?

  • 除0错误

当进程执行代码的时候,我们知道,CPU中的eip或者pc指针会保存代码的下一条指令的地址;其中还有一种寄存器叫做状态寄存器,其中有一个比特位表示状态标志位,称为溢出标志位,当我们发生除0的时候,在CPU中会进行计算,但是除0之后数字变成非常大,这个溢出标志位就会溢出了,由0变成1;我们还要知道,整个CPU中的数据其实都属于当前进程的上下文,我们以前也介绍过,也就是虽然CPU只有一个,但是CPU中的数据可以有无数份,所以硬件只有一套,但是进程在被调度期间,CPU里放的都属于当前进程的上下文!所以CPU在进行调度运行的时候,一旦出现异常了,对应的状态寄存器由0置1了,该进程是否出异常与进程切换无关,也就是说,该进程必定是出异常了,但是它不会影响其它进程,因为出异常的数据是属于当前进程的上下文,当该进程被切换时,其它进程的上下文会放上CPU上正常运行!

那么当溢出标志位溢出之后,操作系统需要知道CPU出现溢出了吗?计算出错了吗?需要!操作系统在调度进程时必须要知道已经出现异常了,因为操作系统是硬件的管理者!CPU也是硬件!所以操作系统需要时刻知道CPU的状态寄存器的状态!所以当操作系统发现CPU发生了除0溢出,操作系统就会向进程发送信号,然后进程接收到信号崩溃了!

  • 越界访问(野指针)

我们已经知道,进程的地址空间通过页表映射到物理内存,访问自己的代码和数据。如果我们出现野指针,我们当前访问的时候,通过页表完成对虚拟地址到物理地址的转化,查表的过程并不是操作系统直接来查的,因为对于操作系统来说很费时间,效率低下,所以这个过程是由一个叫做 MMU 的硬件(内存)管理单元完成的,它如今是集成在CPU中的。那么我们也知道,CPU里读到的都是虚拟地址,当CPU通过页表转换野指针的物理地址的时候,会转换失败!CPU中还有一个寄存器,当CPU进行对虚拟到物理地址的转换时,当发生转换失败了,它会把转换失败的虚拟地址放到该寄存器中。当转换失败时 MMU 也会发生报错,硬件报错会被操作系统识别到,因为不同种类的CPU报错信息,所以操作系统可以识别是哪种错误,所以此时操作系统就会发送对应的信号给进程!

所以我们捕捉了信号之后,没有退出,为什么会一直死循环不退出呢?因为至始至终,进程引发了硬件异常问题,也没有修正问题,所以硬件异常一直存在,随着进程被调度,上下文错误也一直存在,所以操作系统一直检测到有这个异常,就一直给该进程发信号,而我们也一直在捕捉这个信号没有处理,所以该进程才不会退出!

5. 软件条件

那么异常只会由硬件产生吗?不一定,比如我们之前学的管道,当读端关闭后,写端一直在写入的时候,操作系统就会因为效率问题关掉写端,并给写端发送 SIGPIPE 13号信号,这就算一种软件异常。

闹钟

其实软件上不仅仅是可以出异常,也可以出一些特殊事件,我们把这些特殊事件称为软件条件,下面我们介绍一种特殊事件 - - - 闹钟。我们可以给进程设置闹钟,闹钟响了,就可以给进程触发对应的条件,执行对应的动作,这个就称为软件条件。

我们可以看看闹钟的系统调用,alarm()

在这里插入图片描述

alarm() 就是给进程设定一个闹钟,一旦闹钟响了,就会给进程发送信号。参数就是我们设定的时间,单位为秒。那么 alarm() 发送的信号是信号编号中的 SIGALRM 14号信号。

其中返回值我们要理解一下,当我们设定好闹钟,我们可能会提前醒来,那么进程也是一样,当进程被提前发送了14号信号,就相当于提前醒来,那么返回值就是上一次设定闹钟的剩余时间。

假设我们设定一个5秒的闹钟,如下:

				int main()
				{
				    int n = alarm(5);
				    while(1)
				    {
				        cout << "i am a process..." << endl;
				        sleep(1);
				    }
				
				    return 0;
				}

在这里插入图片描述

我们可以捕捉该信号验证一下,注意我们上面只设定了一次闹钟,一旦响过之后就不会再响了,所以我们下面验证的时候再设定每隔五秒响一次,如下:

				void headler(int signo)
				{
				    cout << "... get a signal: " << signo << endl;
				    int n = alarm(5);
				}

				int main()
				{
				    int n = alarm(5);
				    signal(14, headler);
				    while(1)
				    {
				        cout << "i am a process..." << endl;
				        sleep(1);
				    }
				    return 0;
				}

在这里插入图片描述

其中我们查看详细的信号信息的时候,如下图,发现闹钟在 Action 这一列中是 Term

在这里插入图片描述

而有些信号却是 Core,下面我们说一下这两个的区别。我们在以前学习进程控制的时候,进程在正常终止的时候,它会有自己的退出状态,也就是wait时,获取的 status 参数,这个 status 一共是32个比特位,我们只使用低十六位,其中我们通过次低八位,用来表示进程退出时的退出码;而一旦异常了会收到退出信号,退出信号是低七位比特位;而还有一位是 core dump,我们并没有介绍,而这个字段就是当进程在终止的时候,这个标志位只有一个比特位,为0或者1,它是用来表示进程是 Term 这种终止方式还是 Core 这种终止方式。

在这里插入图片描述

下面我们获取一下验证一下:

				int main()
				{
				    pid_t id = fork();
				    // child
				    if(id == 0)
				    {
				        int cnt = 50;
				        while(cnt--)
				        {
				            cout << "i am a child process, pid: " << getpid() << endl;
				            sleep(1);
				        }
				        exit(0);
				    }
				
				    // father
				    int status = 0;
				    pid_t rid = waitpid(id, &status, 0);
				    if(rid == id)
				    {
				        cout << "child quit info, rid: " << rid 
				            << ", exit core: " << ((status >> 8) & 0xFF) 
				            << ", exit signal: " << (status & 0x7F) 
				            << ", core dump: " << ((status >> 7) & 1) << endl;
				    }
				
				    return 0;
				}

下面我们使用 2 号信号杀掉子进程,因为 2 号信号是 Term 的终止方式:

在这里插入图片描述

那么我们看到 core dump 是0;接下来我们使用 8 号信号杀掉子进程,因为 8 号信号是以 Core 方式终止进程的:

在这里插入图片描述

但是我们发现,core dump 也是0,这是为什么呢?这是因为默认云服务器上的 core 功能是被关闭的!虚拟机是默认没有关闭的。我们可以使用指令 ulimit -a 查看系统中一些标准的配置,其中有一个叫做 core file size 的选项,它默认是0的:

在这里插入图片描述

我们也可以使用 ulimit -c 直接查看它,这就是 core 功能:

在这里插入图片描述

我们可以使用 ulimit -c size 设置它,如下:

在这里插入图片描述

如上,core file size 的大小就被我们设置成 1024 了;

此时当我们再次测试上面的结果的时候,我们使用 2 号信号杀掉子进程 core dump 还是 0,但是使用 8 号信号杀掉子进程的时候 core dump 就变成了 1. 如下:

在这里插入图片描述

而且我们在当前目录下多了一个文件!如下:

在这里插入图片描述

所以我们得出结论,打开系统中的 core dump 功能,一旦进程异常退出,操作系统会将进程在内存中的运行信息,给我们 dump(转储) 到进程的当前目录!形成的 core.pid 文件的过程就是核心转储

那么为什么要进行核心转储呢?其实当发生核心转储时,一定发生了运行时错误,当发生了运行时错误,我们肯定最想知道发生了什么错误,而且更想知道代码在哪一行出错了!所以这个 core.pid 可以告诉我们代码哪一行出错了!我们先在 makefile 中加上 -g 选项,让该程序可以被调试。然后我们测试一下,使用除0错误的代码测试:

				int main()
				{
				    int a = 2, b = 0;
				    cout << "...before " << endl;
				    a /= b;
				    cout << "...after " << endl;
				    return 0;
				}

此时我们再运行程序,发现这次报错中后面多了个括号,表示当前已经被核心转储了:

在这里插入图片描述

那么接下来我们想知道哪一行出错,就可以直接开始调试,再输入 core-file core.pid 直接将我们的 core.pid 文件导进来即可,如下:

在这里插入图片描述

所以 core.pid 就是直接复现问题之后,直接定位到出错行,这种先运行,再 core-file 的我们称为事后调试。

那么云服务器上为什么要默认关闭 core dump 呢?我们可以看到形成的 core.pid 相对于其它文件非常大,而且我们的代码量还不大,如下:

在这里插入图片描述

但是当在服务器中,服务器挂掉后,会自动重启,但是如果一个服务器有问题,一启动就挂,又重启,那么一直重复的话,如果 core dump 打开,那么磁盘就有可能被 core.pid 文件打满了,此时的影响就更大了!所以 core dump 是默认被关闭的。

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

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

相关文章

【设计模式】23中设计模式笔记

设计模式分类 模板方法模式 核心就是设计一个部分抽象类。 这个类具有少量具体的方法&#xff0c;和大量抽象的方法&#xff0c;具体的方法是为外界提供服务的点&#xff0c;具体方法中定义了抽象方法的执行序列 装饰器模式 现在有一个对象A&#xff0c;希望A的a方法被修饰 …

离线场景下任意文档的在线预览及原样格式翻译,不依赖其他厂商接口非侵入式一行js代码实现网站的翻译及国际化,可配置使用多种翻译语言

离线场景下任意文档的在线预览及原样格式翻译&#xff0c;不依赖其他厂商接口非侵入式一行js代码实现网站的翻译及国际化&#xff0c;可配置使用多种翻译语言。 要实现翻译需要解决以下3个主要问题&#xff1a; 1&#xff09;from&#xff1a;内容本身的语言类型是什么&#xf…

Open CASCADE学习|扫掠

目录 1、BRepPrimAPI_MakePrism Draw Test Harness&#xff1a; C&#xff1a; 2、BRepPrimAPI_MakeRevol Draw Test Harness&#xff1a; C&#xff1a; 3、BRepOffsetAPI_MakePipeShell Draw Test Harness&#xff1a; C&#xff1a; Draw Test Harness&#xff1a;…

node.js+vue企业人事自动化办公oa系统c288a

采用B/S模式架构系统&#xff0c;开发简单&#xff0c;只需要连接网络即可登录本系统&#xff0c;不需要安装任何客户端。开发工具采用VSCode&#xff0c;前端采用VueElementUI&#xff0c;后端采用Node.js&#xff0c;数据库采用MySQL。 涉及的技术栈 1&#xff09; 前台页面…

小程序-云开发 获取用户的openid等信息

说明介绍&#xff1a; 小程序云开发功能来获取用户的openid。 一般在我们需要用到用户登录的时候&#xff0c;通常是需要获取微信小程序的openid的&#xff0c;由于微信的限制&#xff0c;一般我们只能通过后台去调微信的接口&#xff0c;来授权获取&#xff0c;增加了后端开发…

OnlyOffice-8.0版本深度测评

OnlyOffice 是一套全面的开源办公协作软件&#xff0c;不断演进的 OnlyOffice 8.0 版本为用户带来了一系列引人瞩目的新特性和功能改进。OnlyOffice 8.0 版本在功能丰富性、安全性和用户友好性上都有显著提升&#xff0c;为用户提供了更为强大、便捷和安全的文档处理和协作环境…

内网安全-内网穿透

目录 内网渗透 Nc使用详解 Nc监听和探测 Nc传文件 termite内网穿透工具 ssh代理内网穿透 ssh配置socket代理 MSF多级网络穿透 内网渗透 Nc使用详解 Nc监听和探测 Nc传文件 termite内网穿透工具 1、termite 之前叫ew &#xff08;可以进行正向连接&#xff0c;可以…

【深度学习】“智能皮肤:深度学习驱动的‘智慧之眼‘应用如何革新皮肤病诊疗未来“

在一个不久的未来世界&#xff0c;医疗科技取得了惊人的突破。一款名为“智慧之眼”的神秘应用横空出世&#xff0c;它如同科幻小说中的神器&#xff0c;能够通过摄像头扫描皮肤病变&#xff0c;并借助深度学习技术迅速得出专业级别的诊断结果。这个革新性的故事始于一场科研马…

【制作100个unity游戏之23】实现类似七日杀、森林一样的生存游戏10(附项目源码)

本节最终效果演示 文章目录 本节最终效果演示系列目录前言快捷栏绘制UI代码控制快捷列表信息 源码完结 系列目录 前言 欢迎来到【制作100个Unity游戏】系列&#xff01;本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第23篇中&#xff0c;我们将探索如何制作…

Java异常处理 throw和throws

目录 throwthrows实例制造异常 在Java中&#xff0c;throw和throws关键字都与异常处理有关&#xff0c;但它们的使用方式和目的有所不同。 throw throw关键字&#xff1a; * throw用于在代码中显式地抛出一个异常。你可以使用它来触发一个异常&#xff0c;并指定异常的类型。…

FPGA_简单工程_VGA显示驱动器

一 理论 使用640*48060显示模式&#xff0c;将数字信号转换位模拟信号&#xff0c;经由VGA进行显示。 使用3GM723&#xff0c;3路高清视频编码芯片。 3GM7123编码芯片&#xff1a; 该芯片的主要功能是将RGB888的颜色数据转换成模拟的电压信号&#xff0c;然后进入到VGA接口的…

STM32CubeMX,定时器之定时功能,入门学习,如何设置prescaler,以及timer计算PWM输入捕获方法(重要)

频率变小&#xff0c;周期变长 1&#xff0c;参考链接&#xff08;重要&#xff09; STM32CubeMX——定时器之定时功能&#xff08;学习使用timer定时器的设置&#xff09; STM32测量PWM信息&#xff08;学习使用设置pwm输入捕获&#xff09; 通用定时器中两个重要参数的设置心…

吹响AI PC号角!微软在Windows中不断增加“Copilot含量”

2024&#xff0c;会是AI PC元年吗&#xff1f;至少微软正在往这个方向努力。 本周&#xff0c;微软开始在Windows中测试Copilot的“新体验”&#xff0c;其中包括任务栏中的Copilot图标&#xff0c;当用户复制文本或图片时&#xff0c;Copilot操作菜单就会自动出现。 有媒体在…

《CSS 简易速速上手小册》第5章:CSS 动画与过渡(2024 最新版)

文章目录 5.1 CSS 过渡基础&#xff1a;网页的微妙舞步5.1.1 基础知识5.1.2 重点案例&#xff1a;按钮悬停效果5.1.3 拓展案例 1&#xff1a;渐变显示导航菜单5.1.4 拓展案例 2&#xff1a;动态调整元素大小 5.2 关键帧动画&#xff1a;编排你的网页芭蕾5.2.1 基础知识5.2.2 重…

基于vue+node.js的校园跳蚤市场系统多商家

校园跳蚤市场系统可以在短时间内完成大量的数据处理、帮助用户快速的查找校园跳蚤市场相关信息&#xff0c;实现的效益更加直观。校园跳蚤市场系统中采用nodejs技术和mysql数据库。主要包括管理员、发布者和用户三大部分&#xff0c;主要功能是实现对个人中心、用户管理、发布者…

【MATLAB源码-第138期】基于matlab的D2D蜂窝通信仿真,对比启发式算法,最优化算法和随机算法的性能。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 D2D蜂窝通信介绍 D2D蜂窝通信允许在同一蜂窝网络覆盖区域内的终端设备直接相互通信&#xff0c;而无需数据经过基站或网络核心部分转发。这种通信模式具有几个显著优点&#xff1a;首先&#xff0c;它可以显著降低通信延迟&…

大模型训练所需的硬件配置

1. 引入 训练一个大模型&#xff0c;到底需要投入多少块GPU&#xff0c;需要多少数据&#xff0c;训练多长时间能达到一个不错的效果&#xff1f; 本文引用靠谱的数据&#xff0c;来回答这些问题。 2. 全流程训练 大模型的训练&#xff0c;简单来说&#xff0c;分为Pretrain…

C#计算矩形面积:通过定义结构 vs 通过继承类

目录 一、涉及到的知识点 1、结构 2.结构和类的区别 3.继承 4.使用类继承提高程序的开发效率 二、实例&#xff1a;通过定义结构计算矩形面积 1.源码 2.生成效果 三、实例&#xff1a;通过继承类计算梯形面积 1.源码 2.生成效果 一、涉及到的知识点 1、结构 结构是…

git安装配置

1、下载安装 下载地址 2、配置git用户 git config --global user.name "yw" git config --global user.email "88888qq.com" 3、git init 初始化 4、生成ssh密钥 mkdir .ssh //创建文件夹cd .ssh //进入新建文件夹 ssh-keygen -t rsa // 输入密钥文…

推荐系统|召回05_矩阵补充、最近邻查找

文章目录 矩阵补充Matrix Completion模型结构模型训练模型存储 矩阵补充Matrix Completion 模型结构 通过用户ID和物品ID分别找到对应的向量&#xff0c;然后去做内积&#xff0c;内积的数值可以去衡量匹配的程度。 不共享参数的意思是指用户ID和物品ID使用不同的Embedding L…
最新文章