深入理解Linux内核--信号
信号的作用
信号(signal)是很短的消息,可以被发送到一个进程或一组进程。
使用信号的两个主要目的是:
1.让进程知道已经发生了一个特定的事件。
2.强迫进程执行它自己代码中的信号处理程序。
POSIX标准还引入了一类新的信号,叫做实时信号(real-time signal);
在Linux中它们的编码范围为32~64。它们与常规信号有很大的不同,
因为它们必须排队以便发送的多个信号能被接收到。
另一方面,同种类型的常规信号并不排队:
如果一个常规信号被连续发送多次,
那么,只有其中的一个发送到接收进程。
尽管Linux内核并不使用实时信号,它还是通过几个特定的系统调用完全实现了POSIX标准。信号的一个重要特点是它们可以随时被发送给状态经常不可预知的进程。
发送给非运行进程的信号必须由内核保存,直到进程恢复执行。
阻塞一个信号(后面描述)要求信号的传递拖延,直到随后解除阻塞,
这使得信号产生一段时间之后才能对其传递这一问题变得更加严重。
因此,内核区分信号传递的两个不同阶段:
1.信号产生
内核更新目标进程的数据结构以表示一个信号已经被发送
2.信号传递
内核强迫目标进程通过以下方式对信号做出反应:
或改变目标进程的执行状态,或开始执行一个特定的信号处理程序,或两者都是。信号一旦已传递出去,进程描述符中有关这个信号的所有信息都被取消。
已经产生但还没有传递的信号称为挂起信号(pending signal)。
任何时候,一个进程仅存在给定类型的一个挂起信号,
同一进程同种类型的其他信号不被排队,只被简单地丢弃。
但是,实时信号是不同的:同种类型的挂起信号可以有好几个。
一般来说,信号可以保留不可预知的挂起时间。必须考虑下列因素:
1.信号通常只被当前正运行的进程传递(即由current进程传递)。
2.给定类型的信号可以由进程选择性地阻塞(blocked).
3.当进程执行一个信号处理程序的函数时,通常“屏蔽”相应的信号,
即自动阻塞这个信号直到处理程序结束。
因此,所处理的信号的另一次出现不能中断信号处理程序,
所以,信号处理函数不必是可重入的。内核必须:
1.记住每个进程阻塞哪些信号。
2.当从内核态切换到用户态时,
对任何一个进程都要检查是否有一个信号已到达。
3.确定是否可以忽略信号。这发生在下列所有的条件都满足时:
3.1.目标进程没有被另一个进程跟踪(进程描述符中ptrace字段的PT_PTRACED标志等于0)。
3.2.信号没有被目标进程阻塞。
3.3.信号被目标进程忽略
4.处理这样的信号,
即信号可能在进程运行期间的任一时刻请求把进程切换到一个信号处理函数,
并在这个函数返回以后恢复原来执行的上下文。
传递信号之前所执行的操作
进程以三种方式对一个信号做出应答:
1. 显式地忽略信号。
2. 执行与信号相关的缺省操作
Terminate进程被终止。
Dump进程被终止,并且,如果可能,创建包含进程执行上下文的核心转储文件;
lgnore信号被忽略。
Stop进程被停止,即把进程置为TASK_STOPPED状态
Continue如果进程被停止(TASK_STOPPED)。就把它置为TASK_RUNNING状态。
3. 通过调用相应的信号处理函数捕获信号。如果一个进程正在被跟踪时接收到一个信号,
内核就停止这个进程,
并向跟踪进程发送一个SIGCHLD信号以通知它一下。
跟踪进程又可以使用SIGCOUNT信号重新恢复被跟踪进程的执行。SIGKILL和SIGSTOP信号不可以被显式地忽略、捕获或阻塞,
因此,通常必须执行它们的缺省操作。
POSIX信号和多线程应用
POSIX 1003.1标准对多线程应用的信号处理有一些严格的要求:
1.信号处理程序必须在多线程应用的所有线程之间共享;
不过,每个线程必须有自己的挂起信号掩码和阻塞信号掩码。
2.POSIX库函数kill()和sigqueue()必须向所有的多线程应用而不是某个特殊的线程发送信号。
所有由内核产生的信号同样如此(如:SIGCHLD、SIGINT或SIGQUIT)。
3.每个发送给多线程应用的信号仅传送给一个线程,
这个线程是由内核在从不会阻塞该信号的线程中随意选择出来的。
4.如果向多线程应用发送了一个致命的信号,
那么内核将杀死该应用的所有线程,而不仅仅是杀死接收信号的那个线程。
有两个例外:不可能给进程0(swapper)发送信号,
而发送给进程1(init)的信号在捕获到它们之前也总被丢弃。
因此,进程0永不死亡,而进程1只有当init程序终止时才死亡。如果一个挂起信号被发送给了某个特定进程,那么这个信号是私有的;
如果被发送给了整个线程组,它就是共享的。
与信号相关的数据结构
blocked字段存放进程当前所屏蔽的信号。
它是一个sigset_t位数组,每种信号类型对应一个元素:
信号的编号对应于sigset_t类型变量中的相应位下标加1。
信号描述符和信号处理程序描述符
信号描述符被属于同一线程组的所有进程共享,信号描述符中与信号处理有关的字段如表11-4所示:
sigaction数据结构
sa_handler指向信号处理程序的一个指针/SIG_DFL/SIG_IGN
sa_flags这是一个标志集
sa_mask当运行信号处理程序时要屏蔽的信号。
挂起信号队列
有几个系统调用能产生发送给整个线程组的信号,如kill()和rt_sigqueueinfo(),
而其他的一些则产生发送给特定进程的信号,如tkill()和tgkill()。
内核把两个挂起信号队列与每个进程相关联:
1.共享挂起信号队列,
存放整个线程组的挂起信号。
2.私有挂起信号队列,
存放特定进程(轻量级进程)的挂起信号。挂起信号队列由sigpending数据结构组成,它的定义如下:
siginfo_t是一个128字节的数据结构,其中存放有关出现特定信号的信息。
它包含下列字段:
si_signo信号编号。
si_errno引起信号产生的指令的出错码,或者如果没有错误则为0。
si_code发送信号者的代码(参见表11-8)。
产生信号
很多内核函数都会产生信号:即根据需要更新一个或多个进程的描述符。它们不直接执行第二步的信号传递操作,而是可能根据信号的类型和目标进程的状态唤醒一些进程,并促使这些进程接收信号。
表11-9中的所有函数在结束时都调用specific_send_sig_info()函数
表11-10中的所有函数在结束时都调用group_send_sig_info()函数
specific_send_sig_info()函数
specific_send_sig_info()函数向指定进程发送信号,它作用于三个参数:
必须在关本地中断和已经获得t->sighand->siglock自旋锁的情况
下调用specific_send_sig_info()函数。
函数执行下面的步骤:
1. 检查进程是否忽略信号,如果是就返回0(不产生信号)。
当下面的三个忽略信号的条件全部满足时,信号就被忽略:
· 进程没有被跟踪(t->ptrace中的PT_PTRACED标志被清0)
. 信号没有被阻塞(sigismember(&t->blocked,sig)返回0)
· 或者显式地忽略信号(t->sighand->action[sig-1]的sa_handler字段等于SIG_IGN),
或者隐含地忽略信号(sa_handler字段等于SIG_DFL
而且信号是SIGCONT、SIGCHLD、SIGWINCH或SIGURG)
2. 检查信号是否是非实时的(sig<32),
而且是否在进程的私有挂起信号队列上已经有另外一个相同的挂起信号
如果是,就什么都不需要做,因此返回0。
3. 调用send_signal( sig,info,t,&t->pending)
把信号添加到进程的挂起信号集合中
4. 如果send_signal()成功地结束,
而且信号不被阻塞(sigismember(&t->blocked,sig)返回0),
就调用signal_wake_up()函数通知进程有新的挂起信号。
随后,该函数执行下述步骤:
a.把t->thread_info->flags中的TIF_SIGPENDING标志置位。
b.如果进程处于TASK_INTERRUPTIBLE或TASK_STOPPED状态,
而且信号是SIGKILL,就调用try_to_wake_up()唤醒进程。
c.如果try_to_wake_up()返回0,那么说明进程已经是可运行的:
这种情况下,它检查进程是否已经在另外一个CPU上运行,
如果是就向那个CPU发送一个处理器间中断,
以强制当前进程的重新调度。
因为在从调度函数返回时,每个进程都检查是否存在挂起信号,
因此,处理器间中断保证了目标进程能很快注意到新的挂起信号。
5.返回1(已经成功地产生信号)。
send_signal()函数
send_sigmal()函数在挂起信号队列中插入一个新元素,
它接收信号编号sig、siginfo_t 数据结构的地址info(或一个特殊编码,
见上一节对specific_send_sig_info()的描述)、
目标进程描述符的地址t以及挂起信号队列的地址signals作为它的参数。
函数执行下面的步骤:
1. 如果info的值是2,这个信号就是SIGKILL或SIGSTOP,
而且已经由内核通过force_sig_specific()函数产生:
在这种情况下,函数跳转到第9步,
内核立即强制执行与这些信号相关的操作,
因此函数不用把信号添加到挂起信号队列中。
2. 如果进程拥有者的挂起信号的数量(t->user->sigpending)
小于当前进程的资源限制(t->signal->rlim[RLIMIT_SIGPENDING].rlim_cur),
函数就为新出现的信号分配sigqueue数据结构。
q = kmem_cache_alloc(sigqueue_cachep,GFP_ATOMIC);
3. 如果进程拥有者的挂起信号的数量太多,或者上一步的内存分配失败,
就跳转到第9步。
4. 递增拥有者挂起信号的数量
(t->user->sigpending)和t->user所指向的每用户数据结构的引用计数器。
5. 在挂起信号队列signals中增加sigqueue数据结构。
list_add_tail(&q->list,&signals->list);
6. 在sigqueue数据结构中填充表siginfo_t。
if((unsigned long)info == 0){q->info.si_signo = sig;q->info,si_errno = 0;q->info.si_code = SI_USER;q->info._sifields._kill.pid = current->pid;q->info._sifields._kill._uid = current->uid;
} else if((unsigned long)info == 1){q->info.si_signo = sig;q->info.si_errno = 0;q->info.si_code = SI_KERNEL;q->info._sifields._kill.pid = 0;q->info._sifields._kill._uid = 0;
} elsecopy_siginfo(&q->info,info);
copy_siginfo()函数复制由调用者传递的siginfo_t表。
7.把队列位掩码中与信号相应的位置1:
sigaddset(&signals->signal,sig);
8. 返回0:说明信号已被成功地追加到挂起信号队列中。
9. 此时,不再向信号挂起队列中增加元素,因为已经有太多的挂起信号,
或已经没有可以分给sigqueue数据结构的空闲空间,
或者信号已经由内核强制立即发送。
如果信号是实时的,并已经通过内核函数发送给队列排队,
则send_signal()函数返回错误代码-EAGAIN:
if(sig>=32 && info k&(unsigned long)info !=1 && info->si_code != SI_USER)return -EAGAIN;
10.设置队列的位掩码中与信号相关的位:
sigaddset(&signals->signal,sig);
11.返回0:即使信号没有被追加到队列中,挂起信号掩码中相应的位也被设置。
即使在挂起队列中没有空间存放相应的挂起信号,
让目标进程能接收信号也是至关重要的。
假设一个进程正在消耗过多内存的情形。内核必须保证即使没有空闲内存,
ki11()系统调用也能够成功执行,
否则,系统管理员就没有机会通过终止有害进程来恢复系统。
group_send_sig_info()函数
group_send_sig_info()函数向整个线程组发送信号。
它作用于三个参数:信号编号sig、siginfo_t表的地址info(可选的值为0、1或2,
如前面“specific_send_sig_info()函数“一节中所描述的)以及进程描述符的地址p。
该函数主要执行下面的步骤:
1. 检查参数sig是否正确:
if(sig<0 Il sig>64)return -EINVAL;
2. 如果信号是由用户态进程发送的,则该函数确定是否允许这个操作。
下列条件中至少有一个成立时信号才能被传递:
. 发送进程的拥有者具有适当的权能(这通常意味着通过系统管理员发布信号)。
· 信号为SIGCONT且目标进程与发送进程处于同一个注册会话中。
· 两个进程属于同一个用户。
如果不允许用户态进程发送信号,函数就返回值-EPERM。
3. 如果参数sig的值为0,则函数不产生任何信号,立即返回:
if(!sig ll !p->sighand)return 0;
因为0是无效的信号编码,
用于让发送进程检查它是否有向目标线程组发送信号所必需的特权。
如果目标进程正在被杀死(通过检查它的信号处理程序描述符是否已经被释放来获知),
那么函数也返回。
4. 获取p->sighand->siglock自旋锁并关闭本地中断。
5. 调用handle_stop_signal()函数,
该函数检查信号的某些类型,
这些类型可能使目标线程组的其他挂起信号无效。
handle_stop_signal()函数执行下面的步骤:
a.如果线程组正在被杀死
(信号描述符的flags字段的SIGNAL_GROUP_EXIT标志被设置),则函数返回。
b.如果sig是SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号,
就调用rm_from_queue()函数从共享挂起信号队列p->signal->shared_pending
和线程组所有成员的私有信号队列中删除SIGCONT信号。
c.如果sig是SIGCONT信号,
就调用rm_from_queue()函数从共享挂起信号队列p->signal->shared_pending
中删除所有的SIGSTOP、 SIGTSTP、SIGTTIN和SIGTTOU信号,
然后从属于线程组的进程的私有挂起信号队列中删除上述信号,并唤醒进程:
rm_from_queue(0x003c0000,&p->signal->shared_pending);
t = p;
do {rm_from_queue(0x003c0000,&t->pending);try_to_wake_up(t,TASK_STOPPED,0);t = next_thread(t);
} while(t != p);
掩码0x003c0000选择以上四种停止信号。
宏next_thread每次循环都返回线程组中不同轻量级进程的描述符地址
6. 检查线程组是否忽略信号,如果是就返回0值(成功)。
如果在前面“信号的作用”一节中所提到的忽略信号的三个条件都满足
(也可参见前面“specific-send-sig.info()函数”一节中的第1步),就忽略信号。
7. 检查信号是否是非实时的,
并且在线程组的共享挂起信号队列中已经有另外一个相同的信号,
如果是,就什么都不需要做,因此返回0值(成功)。
if(sig<32 && sigismember(&p->signal->shared_pending.signal,sig))return 0;
8. 调用send_signal()函数把信号添加到共享挂起信号队列中
(参见前面“send_signal()函数”一节)。
如果send_signal()返回非0的错误代码,则函数终止并返回相同的值。
9. 调用__group_complete_signal()函数唤醒线程组中的一个轻量级进程
10.释放p->sighand->siglock自旋锁并打开本地中断。
11.返回0(成功)。
函数__group_complete_sigmal()扫描线程组中的进程,
查找能接收新信号的进程。
满足下述所有条件的进程可能被选中:
. 进程不阻塞信号。
. 进程的状态不是EXIT_ZOMBIE、EXIT_DEAD、TASK_TRACED
或TASK_STOPPED
(作为一种异常情况,如果信号是SIGKILL,
那么进程可能处于TASK_TRACED或者TASK_STOPPED状态)。
. 进程没有正在被杀死,即它的PF_EXITING标志没有置位。
. 进程或者当前正在CPU上运行,或者它的TIF_SIGPENDING标志还没有设置。
(实际上,唤醒一个有挂起信号的进程是毫无意义的:
通常,唤醒操作已经由设置了TIF_SIGPENDING标志的内核控制路径执行;
另一方面,如果进程正在执行,则应该向它通报有新的挂起信号。)
一个线程组可能有很多满足上述条件的进程,函数按照下面的规则选择其中的一个进程:
. 如果p标识的进程(由group_send_sig_info()的参数传递的描述符地址)满足所有的优先准则,
并因此而能接收信号,函数就选择该进程。
否则,函数通过扫描线程组的成员搜索一个适当的进程,
搜索从接收线程组最后一个信号的进程(p->signal->curr_target)开始。
如果函数__group_complete_signal()成功地找到一个适当的进程,
就开始向被选中的进程传递信号。
首先,函数检查信号是否是致命的,
如果是,通过向线程组中的所有轻量级进程发送SIGKILL信号杀死整个线程组。
否则,函数调用signal_wake_up()函数通知被选中的进程:
有新的挂起信号到来(见前面“specific_send_sig_info()函数”一节的第4步)。
传递信号
相关文章:

深入理解Linux内核--信号
信号的作用 信号(signal)是很短的消息,可以被发送到一个进程或一组进程。 使用信号的两个主要目的是: 1.让进程知道已经发生了一个特定的事件。 2.强迫进程执行它自己代码中的信号处理程序。 POSIX标准还引入了一类新的信号,叫做…...
转圈打印矩阵
转圈打印矩阵 【题目】 给定一个整型矩阵 matrix,请按照转圈的方式打印它。 例如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 打印结果为:1,2,3,4,8,12,16,1…...

Elasticsearch 与 OpenSearch:揭开性能差距
作者:George Kobar, Ugo Sangiorgi 对于任何依赖快速、准确搜索数据的组织来说,强大、快速且高效的搜索引擎是至关重要的元素。 对于开发人员和架构师来说,选择正确的搜索平台可以极大地影响你的组织提供快速且相关结果的能力。 在我们全面的…...
100个Java工具类之41:系统工具类Apache之SystemUtils
系统工具类Apache之 org.apache.commons.lang3.SystemUtils 根据Apache SystemUtils源码中介绍,SystemUtils是java.lang.System的帮助程序。当因安全限制无法读取系统属性时,则会返回null。下面是为大家整理的几个主要用法。 一、获取主机名 String ho…...

maven Jar包反向install到本地仓库
maven Jar包反向install到本地仓库 需求实现 需求 项目打包时报错,缺少一个jar包。 但是在maven仓库都找不到此jar包,其他人提供了这个jar包。 需要把这个jar包install到本地仓库,使项目能正常打包运行。 实现 使用git bash命令执行以下脚…...

.NET6使用SqlSugar操作数据库
1.//首先引入SqlSugarCore包 2.//新建SqlsugarSetup类 public static class SqlsugarSetup{public static void AddSqlsugarSetup(this IServiceCollection services, IConfiguration configuration,string dbName "ConnectString"){SqlSugarScope sqlSugar new Sq…...

MySQL8是什么-MySQL8知识详解
从今天起,开始更新MySQL8的教程,今天更新MySQL8的第一篇文章,主要讲了MySQL8是什么、MySQL数据库的概念、MySQL的优势和MySQL的发展历史。 1、MySQL8是什么 MySQL 8是一个开源的关系型数据库管理系统。它是MySQL数据库的最新版本,…...

Spring Gateway+Security+OAuth2+RBAC 实现SSO统一认证平台
背景:新项目准备用SSO来整合之前多个项目的登录和权限,同时引入网关来做后续的服务限流之类的操作,所以搭建了下面这个系统雏形。 关键词:Spring Gateway, Spring Security, JWT, OAuth2, Nacos, Redis, Danymic datasource, Jav…...

flutter开发实战-TextPainter计算文本内容的宽度
flutter开发实战-TextPainter计算文本内容的宽度 最近开发过程中根据Text文本的大小判断是否需要进行显示跑马灯效果,获取文本的大小,需要TextPainter来获取Size 一、TextPainter TextPainter主要用于实现文本的绘制。TextPainter类可以将TextSpan渲染…...

竞赛项目 深度学习的动物识别
文章目录 0 前言1 背景2 算法原理2.1 动物识别方法概况2.2 常用的网络模型2.2.1 B-CNN2.2.2 SSD 3 SSD动物目标检测流程4 实现效果5 部分相关代码5.1 数据预处理5.2 构建卷积神经网络5.3 tensorflow计算图可视化5.4 网络模型训练5.5 对猫狗图像进行2分类 6 最后 0 前言 &#…...
MySQL相关的SQL语句、数据库、数据表、字段、类型
文章目录 前言MySQL整数类型MySQL字符串类型MySQL小数类型MySQL时间类型常用的基本SQL语句 前言 1、SQL语句不区分大小写。 MySQL整数类型 序号数据类型数据范围1TINYINT-128~1272SMALLINT-32768~327673MEDIUMINT-223~223-14INT-231~231-15BIGINT-263~263-1 MySQL字符串类型 …...

微信个人小程序申请 (AppID 和 AppSecret)
1. 登录微信公众平台 https://mp.weixin.qq.com/cgi-bin/loginpage?url%2Fcgi-bin%2Fhome%3Ft%3Dhome%2Findex%26lang%3Dzh_CN%26token%3D47421820 2. 右上角立即注册 3. 注册类型选择小程序 4. 账号信息 5. 邮箱激活 6. 小程序发布流程 7. 小程序信息 (前往填写) 8. 获取小程…...
使用zap日志替代xorm日志
xorm提供了自定义日志的接口,它的接口定义如下: // Logger is a logger interface type Logger interface {Debug(v ...interface{})Debugf(format string, v ...interface{})Error(v ...interface{})Errorf(format string, v ...interface{})Info(v ..…...

YOLOv5-7.0实例分割+TensorRT部署
一:介绍 将YOLOv5结合分割任务并进行TensorRT部署,是一项既具有挑战性又令人兴奋的任务。分割(Segmentation)任务要求模型不仅能够检测出目标的存在,还要精确地理解目标的边界和轮廓,为每个像素分配相应的…...

回归决策树模拟sin函数
# -*-coding:utf-8-*- import numpy as np from sklearn import tree import matplotlib.pyplot as pltplt.switch_backend("TkAgg") # 创建了一个随机数生成器对象 rng rngnp.random.RandomState(1) print("rng",rng) #5*rng.rand(80,1)生成一个80行、1列…...
NeRF基础代码解析
embedders 对position和view direction做embedding。 class FreqEmbedder(nn.Module):def __init__(self, in_dim3, multi_res10, use_log_bandsTrue, include_inputTrue):super().__init__()self.in_dim in_dimself.num_freqs multi_resself.max_freq_log2 multi_resself…...

职场新星:Java面试干货让你笑傲求职路(三)
职场新星:Java面试干货让你笑傲求职路 1、token 为什么存放在 redis 中?2、索引的底层原理是什么?3、Spring IOC和AOP的原理4、接口和抽象类有什么共同点和区别?5、为什么要使用线程池?直接new个线程不好吗?…...
获取指定收获地址的信息
目录 1 /// 获取指定收获地址的信息 2 /// 删除指定的收获地址信息 3 /// 取消订单 4 /// 确认订单收货 /// <summary> /// 获取指定收获地址的信息</...

突破笔试:力扣全排列(medium)
1. 题目链接:46. 全排列 2. 题目描述:给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1: 输入:nums [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[…...

gitlab 503 错误的解决方案
首先使用 sudo gitlab-ctl status 命令查看哪些服务没用启动 sudo gitlab-ctl status 再用 gitlab-rake gitlab:check 命令检查 gitlab。根据发生的错误一步一步纠正。 gitlab-rake gitlab:check 查看日志 tail /var/log/gitlab/gitaly/current删除gitaly.pid rm /var/opt…...

C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1
每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...