[Linux] 进程信号概念 | 信号产生

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

          主厨:邪王真眼

主厨的主页:Chef‘s blog  

所属专栏:青果大战linux

总有光环在陨落,总有新星在闪烁

为什么我的课设这么难啊,久久叔叔吧,悲,模电要挂了


信号的概念

信号和信号量没有任何关系,他们就是老婆和老婆饼的关系

信号是进程之间事件异步通知的一种方式,属于软中断。
  • 同步(Synchronous)
    • 定义:同步操作是一种按照顺序依次执行的方式。在同步模式下,一个任务必须等待前一个任务完成后才能开始。可以把它想象成一个餐厅,顾客(程序)点完菜(发起任务)后,顾客什么都不干,等菜上来了,开始吃饭,这就是同步
  • 异步(Asynchronous)
    • 定义:任务的发起和完成不需要严格按照顺序。当一个异步任务被发起后,程序不会等待这个任务完成,而是可以继续执行其他任务。当异步任务完成时,会通过某种方式(如回调函数、事件通知等)通知程序。可以把它想象成一个餐厅,顾客(程序)点完菜(发起任务)后,可以做其他事情,比如聊天、看手机,等菜做好了(任务完成),服务员会通知顾客,这就是异步
    • 示例:在 JavaScript 中,使用setTimeout()函数就是一种异步操作。例如,setTimeout(() => console.log("Hello"), 1000);会在 1 秒后打印 “Hello”,但是在这 1 秒内,程序可以继续执行其他代码,而不是等待这个打印操作。
  • 因为信号也是由进程发送的,所以当一个进程正常运行的时候,系统收到了比如杀死这个进程的信号,那么就会有一个进程A作为信号去终止该进程B。但是B进程是不会等信号来的,而是一直做自己的事情。

可以通过指令kill -l来查询linux所支持的常见信号:

这里的信号如一号信号SIGHUP都属于宏,他们的值就是他们的编号,SIGHUP的值就是1. 

  • [1, 31]:这些信号称为非实时信号,当进程收到这些信号后,可以自己选择合适的时候处理
  • [34, 64]:这些信号称为实时信号,当进程收到这些信号后,必须立马处理
  • 实时操作系统:对外部事件响应有严格时间要求,必须在规定时间内作出响应。在任务调度上,采用优先级抢占式和时间片轮转(同优先级)调度,确保关键任务优先执行。用于工业控制、航空航天、医疗设备等对时间敏感的领域。
  • 非实时操作系统:没有严格时间限制,注重通用功能。在任务调度上,有优先级调度(非严格抢占)和公平共享调度,平衡资源分配。用于个人桌面和部分服务器领域,对响应时间要求不高。

事实上,大多数计算机都是非实时的,因此我们今天只学习非实时信号。

进程是如何认识信号的

进程识别信号,由程序员内置的特性,信号的处理方法,在信号产生前就设置好了

就像你在第一次过马路之前,就先被别人告诉了红灯停,绿灯行的信号处理方法

信号会被立刻处理吗

处理信号,不一定是立即处理的,而是选取一个合适的时候

因为当前做的事情的优先级可能比处理信号这件事更高,

处理信号的方法

  1. 默认方法

  2. 忽略该信号(忽略本身也是一种处理方法!!)

  3. 自定义处理方法

 signal

 signal函数是在 Unix、Linux 等操作系统中用于设置信号处理方式的函数。

参数解释

  1. signum:要设置处理方式的信号编号。

  2. handler:是一个函数指针,指向当接收到signum信号时要执行的函数。这个函数应该有一个int类型的参数(用于接收信号编号),并且返回值为void

  3. 如果将handler参数设置为SIG_DFL,则表示当接收到指定信号时,采用系统默认的处理方式。如果将handler参数设置为SIG_IGN,则表示当接收到指定信号时,进程将忽略该信号。也可以编写一个自定义的函数,然后将函数指针传递给signal函数作为handler参数。

由于信号不一定是被立即处理,所以在信号接受和信号处理之间,还有一个信号保存(或者叫信号记录)的操作,防止进程忘记处理。

信号记录

这里我们要注意,信号的编号是1到31,那么请问如何记录信号呢?位图出场了

因此发送信号的本质就是OS把task_struct中signalbitmap的位图的某个比特位从0置1

当然OS是有这个权力的,毕竟OS是进程的管理者,但是也只有它可以,因为他是唯一管理者

计算机中,无论是硬件还是软件的何种方式发送信号,归根结底到最后都是OS修改位图


信号产生

1.键盘产生

ctrl+c:结束前台进程,后台不行(对前后台不懂的可以去这篇博客前台进程与后台进程)

#include<bits/stdc++.h>
#include<unistd.h>
using namespace std;
int main(){while(true){sleep(1);cout<<"Hello"<<endl;}return 0;
}

显然我们这里输入了ctrl+c,于是进程被结束了。

ctrl+c本质就是向前台进程发送了2号信号(SIGINT)。他的默认处理方式是终止该进程

#include<bits/stdc++.h>
#include<unistd.h>
#include<signal.h>
using namespace std;
void Handler(int sign_num){
cout<<"Get asignal ,it is "<<sign_num<<endl;
}
int main(){signal(2,Handler);while(true){cout<<"hello"<<endl;sleep(1);}return 0;
}

这里我们把二号命令的处理方式修改为了我们自定义的方法,这就是自定义处理,当然也证明了ctrl+c确实是二号命令 

如果你想结束进程可以ctrl+\,他是三号命令

通过man 7 signal可以查看更详细的信号信息

2号和3号的描述是“被键盘中断了”。这里的Core和Term都表示终止进程

我们这里验证一下三号

#include<bits/stdc++.h>
#include<unistd.h>
#include<signal.h>
using namespace std;
void Handler(int sign_num){
cout<<"Get asignal ,it is "<<sign_num<<endl;
}
int main(){signal(2,Handler);signal(3,Handler);while(true){cout<<"hello"<<endl;sleep(1);}return 0;
}

可以看到ctrl+\也结束不了进程了。

signal函数是放在while循环之前的,为什么呢

signal函数对一个信号处理方式的修改只需要设置一次足以,在这之后进程会记录该信号的被修改的处理方式,因此在while循环中,当我们再去ctrl+c时,就会触发被记录的新的处理方式

如果没有产生2号三号信号呢

那么handler函数永远不会被调用

我们的前31号信号大多数都是终止信号,那么要是我们把所有信号都捕捉,按照自定义的方式处理,那我们的死循环代码是不是就无法被结束了

我们在另一个窗口输入kill -n pid,貌似真的杀不了它了

好了逗你的,其实我们的19号命令(SIGSTOP)和9号命令(SIGKILL)是无法被自定义捕捉的,这就是为防止用户把所有能终止的信号都自定义了,然后没法结束进程了

硬件中断

键盘产生信号本质是OS获取并且识别了键盘上的ctrl+c组合键

那么请问,OS怎么知道键盘上有数据了呢,难道是死循环不停的检测键盘的状态?

那么鼠标呢,显示器么,网卡,磁盘呢?难道这些外设都是死循环检测,那会忙死的

ok这里就要来点硬件电路知识了

  • 硬件中断是一种硬件机制,用于通知 CPU(中央处理器)有一个需要立即处理的事件发生。这些事件通常来自外部设备,如键盘、鼠标、磁盘驱动器、网络接口卡等。当外部设备需要 CPU 的注意时,它会发送一个中断信号给 CPU。例如,当你按下键盘上的一个键时,键盘控制器会向 CPU 发送一个中断信号,告诉 CPU 有按键事件发生,CPU 会暂停当前正在执行的任务,cpu会把信号传递给OS,表示键盘资源准备好了,于是OS就会去键盘读取信息,这样OS就不用死循环检测键盘了,其他外设也是同理

可能有细心的同学想到了,之前学了冯诺依曼体系结构,说键盘等外设是不能直接和OS交互的

那么这个硬件中断的信号,是不是违背了冯诺伊曼,

是的,对于该信号,键盘和cpu直接交互了,这个设置你可以理解一种特殊处理

于是硬件就可以和OS并行执行了,OS先给一个硬件发送某种工作信号(比如读写磁盘时要先让磁盘进行寻址工作),然后硬件进行工作,与此同时OS并没等带硬件把工作做完,而是继续忙他自己的事情,比如管理以下文件,给别的软件下达指令,当硬件把事情做完了,就会通过中断告诉OS资源已经就绪,这个时候OS再回来检查结果就好了

至于硬件中断是怎么实现的,先别管了

OS依靠中断管理硬件,同理也可以靠中断管理软件,这种靠中断管理软件的操作就是信号

所以信号本质就是对硬件中断操作的模拟

现在我们再看ctrl+c

键盘按下,向cpu发送硬件中断,cpu去告知OS键盘资源就绪,OS去读取键盘获取了ctrl+c的组合键,将他解释为二号命令,然后把二号命令写入前台进程的signalbitmap位图中,前台进程会等到一个合适的实际去处理该信号


2.指令产生

我们之前的kill -9 [进程pid]就是靠指令产生信号发送给目标进程

kill -n [进程pid]把n号命令发送给目标进程


3.函数调用产生信号

kill

  • pid:收到该信号的进程的pid

  • sig:发送哪一个信号

  • 返回0:发送信号成功

  • 返回-1:发送信号失败

是这样的,kill不但是一个指令,而且是一个系统调用

我们当然也可以自己写一个kill指令

#include<iostream>
#include<unistd.h>
#include<string>
#include<sys/types.h>
#include<signal.h>
using namespace std;
void Usage(string s){
cout<<s<<"-number"<<"  pid"<<endl;
}
int main(int argc,char*argv[]){if(argc!=3){Usage(argv[0]);exit(1);}pid_t id=(pid_t)stoi(argv[2]);int i=(int)(argv[1][1]-'0');kill(id,i);
}

 raise

raise函数用于向调用该函数的进程发送一个信号。 

  1. rig参数:它代表信号编号。

  2. 如果信号发送成功,函数返回 0。

  3. 如果发送信号失败,函数返回一个非零值。


 abort

用于异常终止调用该函数的进程。它会发送SIGABRT信号

当然了,raise和abort底层都是调用了kill系统接口


4.软件产生

由于软件条件(不具备该条件、具备该条件、条件出错等等)而产生信号:

管道

我们学习管道的时候了解到,如果管道的读端已经关闭了,但是写端还没有关闭,那么OS就会直接终止进程,这个终止本质就是发送了13号命令

读端关闭,可以被认为是管道的读写条件没有准备齐全,于是被终止


alarm

设置一个定时器。当定时器超时后,会向调用进程发送一个14号信号(SIGALRM)。默认是终止进程

 

  1. 参数seconds:用于指定定时器的时长,单位是秒
  2. 返回值alarm函数返回上一个定时器剩余的秒数(如果之前设置了定时器)。如果之前没有设置定时器,或者之前设置的定时器已经响了,返回 0。

  3. 当设置的上一个闹钟还没响,就再次使用alarm函数,会用本次设置的闹钟覆盖掉上一个还没响的闹钟

  4. 参数设置为0,表示取消上一个闹钟

 这里并不是设置了1,2,3,4秒各一个闹钟,而是每次设置都更新,最后只设置了一个四秒的闹钟。

#include<unistd.h>
#include<iostream>
int main(){
alarm(3);
while(true){std::cout<<"闹钟没响"<<std::endl;sleep(1);
}
}

alarm可以认为是设置了一个定时器,每个进程都可以设置定时器,定时器可以有很多个,这些定时器会被OS管理起来

struct Timer{pid_t id;//哪个进程设置的定时器struct Timer* Next;int end;//定时器什么时候响(时间戳)//..........
};

我们可以按照时间戳来建一个小堆,这样每次只要看堆顶元素有没有超时即可

实际OS是依靠链表加哈希的方法,但是为了方便大家理解,就当作小堆即可 

alarm所带来的定时器是会被OS先描述再组织的软件数据结构,当这些软件数据结构的信息准备好了(即闹钟时间到了),OS就会向目标进程发送信号,因此闹钟本质属于软件条件是否满足,而决定是否发送信号

如果通过sleep、pause卡住进程一段时间,而闹钟响的时间是在被卡出期间的,那么当进程继续运行时,就会检测到到闹钟时间已经过了但是还没有向进程发出14号命令,于是就会发出14号命令

#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
void handler(int n){std::cout<<"闹钟响了"<<std::endl;
}
int main(){
alarm(2);signal(SIGALRM,handler);sleep(3); int a=alarm(0);std::cout<<a<<std::endl;
}

 alarm是一个一次性的闹钟,当时间到了,她会去执行对应的方法,执行完后就会被取消

如果你想一直执行一个闹钟,那么可以把alarm函数放到对14号信号的自定义捕捉中

我们基于此可以设计一个定时处理任务的程序

pause函数会阻塞进程,当接受到除了9号和19号信号之外的信号时,会取消阻塞。 

#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<functional>
#include<vector>
using func_t =std::function<void()>;
std::vector<func_t>v;
void handler(int n){std::cout<<"闹钟响了,开始写作业"<<std::endl;for(auto &i:v)i();alarm(1);
}int main(){
alarm(2);signal(SIGALRM,handler);v.push_back([](){std::cout<<"我是卑微的高数"<<std::endl;});v.push_back([](){std::cout<<"我是卑微的模电"<<std::endl;});v.push_back([](){std::cout<<"我是卑微的复变函数"<<std::endl;});int a=0;alarm(1);while(true)a++;
}

 ........

突然就笑不出来了(悲,期末周去死啊,我还啥都不会呢

但是,如果我们把代码稍微改改呢

#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<functional>
#include<vector>
#include<stdio.h>
using func_t =std::function<void()>;
std::vector<func_t>v;
void handler(int n){std::cout<<"闹钟响了,OS开始work"<<std::endl;for(auto &i:v)i();//alarm(1);
}int main(){v.push_back([](){std::cout<<"我要刷新内核缓冲区了"<<std::endl;});v.push_back([](){std::cout<<"我要检测时间片是否到了,如果到了就切换进程"<<std::endl;});v.push_back([](){std::cout<<"我要定期清理内存中的垃圾了"<<std::endl;});int a=0;alarm(1);signal(SIGALRM,handler);while(true){  pause();std::cout<<"行啊"<<std::endl;}
}

这时的你,是不是突然发现“原来OS就是这么工作的啊!”

OS就是一个死循环, 他会接受外部的一个固定事件源--时钟中断(集成在cpu内部的),每隔很短的时间他就会想cpu触发硬件中断,OS就是一个中断处理器


5异常产生

我们知道程序除零或者野指针就会崩溃,那么这是为什么呢

因为他们导致进程接受了终止信号。野指针错误对应的是11号信号,除零是8号信号

那要是我们把十一号信号捕捉了呢?

#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<functional>
#include<vector>
#include<stdio.h>
void handler(int num){std::cout<<"我捕捉了"<<num<<"信号"<<std::endl;
}
int main(){signal(11,handler);int* a=nullptr;*a=100;while(1);return 0;
}

 结果是handler函数被疯狂调用,

可是按我们的想法,发现野指针,然后想进城发送11号命令,然后执行我们的自定义捕捉,这不就完了吗,怎么会一直捕捉个不停???

OS怎么知道内部出异常了

对于除零问题

cpu中有一个状态寄存器(EFlags),他可以记录cpu的操作有没有出现错误,他上面有一个比特位是溢出标记位,如果该比特位为1,表示计算结果有问题。当CPU出现计算错误时会通知OS,OS当然要知道这件事,因为OS要管理好硬件。接着OS知道后就会杀掉该进程以维护cpu安全

而我们刚才把OS用来杀死进程的信号进行了自定义捕捉,于是进程没有退出,接着继续在while循环中运行,可是状态寄存器中的溢出标记位没有回复为0,于是进程继续在CPU上跑,CPU发现这家伙状态寄存器有个比特位为1,说明有问题,继续告知OS,OS继续发命令,命令继续被我们自定义捕捉,周而复始,即便你的进程暂时被换出,EFLAGS寄存器的值也会作为进程的上下文数据保存在task_struct中,不会置零,下次进程换入还是要报错。

对于野指针问题

CPU中的CR3寄存器会保存页表

MMU这个单元会在页表中,根据虚拟地址查找对应的物理地址,但是对于NULL我们是没有权限进行转化寻找物理地址的,于是MMU这个硬件会报错,MMU中也有类似于状态寄存器的东西,OS当然要直到这件事,剩下的就和上面一样了。


Core VS Term

Term就是终止,没有别的多余操作

Core:

核心转储。除了退出之外,还会在当前目录形成一个文件core.pid,OS会把进程的部分信息保存下来,方便后序调试debug

但是这个文件一般会被云服务器关闭

通过该指令看出,这个pid.core文件大小被设置为0,所以你看不到这个文件了

ulimit -c 1024

这个指令可以设置pid.core文件的大小

我们再写个有除零或者野指针的代码

这时就会发现出现了core.pid,但是如果你的linux内核比较新,那这个文件名就是core 

为什么云服务器要关了它

我们打开它。对于云服务器,假如你的项目因为除零野指针的问题崩溃了,那么他的debug信息就会被写进core.pid文件中,这个进程也会被终止,但是对于这种放在云服务器上的项目,如果他进程被终止了,我们会选择立即重启(自动),因为要保证24h服务啊,不然就差评满天飞了

那么就会每次重启就挂掉,每次都会生成debug文件,那当你发现时你的磁盘都被打满了,那这就又会影响别的模块的服务了

如果你的linux较新,那么他的debug文件就不是core.pid而是core,因为这样即是程序被重启很多次也会把信息打在一个文件里,每次写入问价都是先刷新再写入,这样磁盘就不会被打满了

使用core.pid进行debug

编译链接记得加入-g选项,对生成的exe进行gdb调试 。

在gdb中输入core-file [core.pid],就有详细的报错信息了,被11信号终止,属于内存错误,在第

22行等等信息 

core_domp

现在我们终于可以回答这个第八位是什么了

第八位表示是否发生了core-dump,他表示子进程是否发生了core_dump,我们直接写一个野指针错误,野指针11号信号,属于core类型

#include<unistd.h>
#include<iostream>
#include<sys/types.h>
#include<signal.h>
#include<functional>
#include<vector>
#include<stdio.h>
#include<wait.h>
int main(){pid_t id=fork();if(id==0){int* a=nullptr;*a=100;}else{int st=0;waitpid(id,&st,0);std::cout<<"core_dump:"<<((st>>7)&1)<<std::endl;}return 0;
}

 也确实生成了新的core文件

子进程是否出现core_dump取决两个条件:

  1. 生成core文件的功能是否被打开

  2. 该进程是否被core命令终止

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小程序测试的测试内容有哪些?

在数字化快速发展的今天&#xff0c;小程序成为了很多企业进行产品推广和服务互动的重要平台。小程序的广泛应用使得对其质量的要求越来越高&#xff0c;小程序测试应运而生。这一过程不仅涉及功能的准确性&#xff0c;更涵盖了用户体验、性能、安全等多个维度。 小程序测试的…

使用 NVIDIA DALI 计算视频的光流

引言 光流&#xff08;Optical Flow&#xff09;是计算机视觉中的一种技术&#xff0c;主要用于估计视频中连续帧之间的运动信息。它通过分析像素在时间维度上的移动来预测运动场&#xff0c;广泛应用于目标跟踪、动作识别、视频稳定等领域。 光流的计算传统上依赖 CPU 或 GP…

微积分复习笔记 Calculus Volume 2 - 4.4 The Logistic Equation

4.4 The Logistic Equation - Calculus Volume 2 | OpenStax

双指针---有效三角形的个数

这里写自定义目录标题 题目链接 [有效三角形的个数](https://leetcode.cn/problems/valid-triangle-number/description/)问题分析代码解决执行用时 题目链接 有效三角形的个数 给定一个包含非负整数的数组 nums &#xff0c;返回其中可以组成三角形三条边的三元组个数。 示例…

【Linux】usb内核设备信息

usb内核设备信息 Linux内核中USB设备信息及拓扑结构可以从/sys/kernel/debug/usb/devices和/sys/bus/usb/devices中获取&#xff0c;下面介绍这些信息如何解读。 通过usbdump函数打印usb信息 [drivers/usb/core/devices.c] #define ALLOW_SERIAL_NUMBER/* Bus: 总线编号 Lev:…

Electron-Vue 开发下 dev/prod/webpack server各种路径设置汇总

背景 在实际开发中&#xff0c;我发现团队对于这几个路径的设置上是纯靠猜的&#xff0c;通过一点点地尝试来找到可行的路径&#xff0c;这是不应该的&#xff0c;我们应该很清晰地了解这几个概念&#xff0c;以下通过截图和代码进行细节讲解。 npm run dev 下的路径如何处理&…

devops和ICCID简介

Devops DevOps&#xff08;Development 和 Operations 的组合&#xff09;是一种软件开发和 IT 运维的哲学&#xff0c;旨在促进开发、技术运营和质量保障&#xff08;QA&#xff09;部门之间的沟通、协作与整合。它强调自动化流程&#xff0c;持续集成&#xff08;CI&#xf…

[HNCTF 2022 Week1]baby_rsa

源代码&#xff1a; from Crypto.Util.number import bytes_to_long, getPrime from gmpy2 import * from secret import flag m bytes_to_long(flag) p getPrime(128) q getPrime(128) n p * q e 65537 c pow(m,e,n) print(n,c) # 62193160459999883112594854240161159…

12.19问答解析

概述 某中小型企业有四个部门&#xff0c;分别是市场部、行政部、研发部和工程部&#xff0c;请合理规划IP地址和VLAN&#xff0c;实现企业内部能够互联互通&#xff0c;同时要求市场部、行政部和工程部能够访问外网环境(要求使用OSPF协议)&#xff0c;研发部不能访问外网环境…

生态学研究中,森林生态系统的结构、功能与稳定性是核心研究

在生态学研究中&#xff0c;森林生态系统的结构、功能与稳定性是核心研究内容之一。这些方面不仅关系到森林动态变化和物种多样性&#xff0c;还直接影响森林提供的生态服务功能及其应对环境变化的能力。森林生态系统的结构主要包括物种组成、树种多样性、树木的空间分布与密度…

springboot445新冠物资管理(论文+源码)_kaic

摘 要 使用旧方法对新冠物资管理的信息进行系统化管理已经不再让人们信赖了&#xff0c;把现在的网络信息技术运用在新冠物资管理的管理上面可以解决许多信息管理上面的难题&#xff0c;比如处理数据时间很长&#xff0c;数据存在错误不能及时纠正等问题。这次开发的新冠物资管…

1.zabbix概述

一、什么是监控 我们的生活里&#xff0c;离不开监控&#xff0c;监控能够最大程度上&#xff0c;发挥如下作用 实时监测&#xff0c;即使你不在电脑前&#xff0c;也能实时掌握监控区域情况&#xff0c;提高工作效率事后录像查询&#xff0c;如果不法事件未能即使发现制止&am…

QT绘图【点】【线】【圆】【矩形】

目录 1. 绘制点、线、圆、文本、矩形3. 调用及更新 1. 绘制点、线、圆、文本、矩形 QPainter painter(this); //实例化绘图 QPen pen(QColor(255,100,155)); //创建绘图工具&#xff08;画笔&#xff09; pen.setWidth(2); //画笔宽度 pen.setStyle(Qt::SolidLine); //实线…

知识分享第三十天-力扣343.(整数拆分)

343 整数拆分 给定一个正整数 n&#xff0c;将其拆分为至少两个正整数的和&#xff0c;并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 示例 1: 输入: 2 输出: 1 解释: 2 1 1, 1 1 1。 示例 2: 输入: 10 输出: 36 解释: 10 3 3 4, 3 3 4 36。 说明: 你可…

NSDT 3DConvert:高效实现大模型文件在线预览与转换

NSDT 3DConvert 作为一个 WebGL 展示平台&#xff0c;能够实现多种模型格式免费在线预览&#xff0c;并支持大于1GB的OBJ、STL、GLTF、点云等模型进行在线查看与交互&#xff0c;这在3D模型展示领域是一个相当强大的功能。 平台特点 多格式支持 NSDT 3DConvert兼容多种3D模型…

USACO备考书籍合集

USACO&#xff0c;全称United States of America Computing Olympiad&#xff0c;即美国计算机奥林匹克竞赛。 以下是网上查到的关于USACO&#xff08;美国计算机奥林匹克竞赛&#xff09;的推荐书籍&#xff1a; 一、国内推荐书籍 有一种观点&#xff0c;冲击USACO铂金&…

汽车IVI中控开发入门及进阶(三十八):HiCar开发

手机投屏轻松实现手机与汽车的无缝连接,导航、音乐、通话等功能应有尽有,还支持更多第三方应用,让车载互联生活更加丰富多彩。 HiCar在兼容性和开放性上更具优势。 手机投屏可以说是车机的杀手级应用,大大拓宽了车机的可用性范围。其中华为推出的HiCar就是非常好用的一种。…

iOS - 超好用的隐私清单修复脚本(持续更新)

文章目录 前言开发环境项目地址下载安装隐私访问报告隐私清单模板最后 前言 在早些时候&#xff0c;提交应用到App Store审核&#xff0c;大家应该都收到过类似这样的邮件&#xff1a; Although submission for App Store review was successful, you may want to correct th…

CSS边框的样式

边框阴影 让元素更有立体感 img {box-shadow: 2px 10px 5px 20px #ff0000;border-radius: 44px;}语法&#xff1a;box-shadow&#xff1a;值1 值2 值3 值4 值5 值1&#xff1a;水平阴影的位置值2&#xff1a;垂直阴影的位置值3&#xff1a;模糊距离值4&#xff1a;阴影的尺寸…

UE5 物体自动跟随主角镜头转向

A、思路 Tick&#xff0c;设置物体世界旋转 旋转数值源于物体自身位置与玩家摄像机位置的差值 效果是物体自动转向&#xff0c;玩家镜头动&#xff0c;则物体也随之调整角度。 适合一些提示文字&#xff0c;如按键提示、帮助之类。 B、参考图