当前位置: 首页 > news >正文

记一次linux操作系统实验

前言

最近完成了一个需要修改和编译linux内核源码的操作系统实验,个人感觉这个实验还是比较有意思的。这次实验总共耗时4天,从对linux实现零基础,通过查阅资料和不断尝试,直到完成实验目标,在这过程中确实也收获颇丰,特此记录

实验内容

  1. 实现系统调用int hide(pid_t pid, int on),在进程pid有效的前提下,如果on置1,进程被隐藏,用户无法通过ps或top观察到进程状态;如果on置0且此前为隐藏状态,则恢复正常状态(考虑权限问题,只有root用户才能隐藏进程)
  2. 设计一个新的系统调用int hide_user_processes(uid_t uid, char *binname),参数uid为用户ID号,当binname参数为NULL时,隐藏该用户的所有进程;否则,隐藏二进制映像名为binname的用户进程
  3. 在/proc目录下创建一个文件/proc/hidden,该文件可读可写,对应一个全局变量hidden_flag,当hidden_flag为0时,所有进程都无法隐藏,即便此前进程被hide系统调用要求隐藏。只有当hidden_flag为1时,此前通过hide调用要求被屏蔽的进程才隐藏起来
  4. 在/proc目录下创建一个文件/proc/hidden_process,该文件的内容包含所有被隐藏进程的pid,各pid之间用空格分开

实现思路

对于要求1,首先要修改PCB,对应到源码里面就是task_struct,在其中添加一个属性hide,用来表示该进程是否需要隐藏;然后修改复制进程的系统调用,用于给hide属性设置默认值0;最后修改列举所有进程的系统调用,在其中加入一个判断,如果进程的hide是1则不展示这个进程
(注:也有方法说是可以通过把pid设置为0来达到隐藏的效果,但是实测下来,在5.15.60的kernel里面,这样做不能隐藏,所以只能通过劫持系统调用来实现)

对于要求2,则可以遍历所有进程,把符合条件的进程的hide设置为1即可

对于要求3,最开始以为可以通过用户态的文件操作来实现,结果后来发现/proc是个虚拟文件系统,所以需要在初始化proc文件系统时,添加一个hide条目,然后设置这个条目的write函数,来达到创建该文件的目的

对于要求4,也是用和要求3一样的思路,只是这里需要设置read函数,然后遍历所有进程,把hide为1的pid全部返回

实验环境

操作系统使用的ubuntu 22.04
linux kernel代码版本是5.15.60
虚拟机使用的是VM Ware Workstation Pro 16
注意:虚拟机硬盘大小建议为60GB,编译内核代码非常吃硬盘,本人在实验中前前后后扩容了几次硬盘,最终发现60GB是个比较合适的大小,内核源码编译安装之后还能剩15GB左右(下一次编译安装还需要一些硬盘空间做缓存,所以剩15GB是比较合适的)

实验流程

编译与安装内核

参考https://www.cnblogs.com/robotech/p/16152269.html 即可,如果这部分出错了,网上可以找到的资料很多,这里不再赘述了
不过这一步一定要有耐心,源码编译很慢,第一次全量编译估计会耗时一个多小时,可以用这个闲暇时间玩玩原神

完成要求一

把编译和安装的流程跑通以后,就开始进行源代码的修改了

修改PCB

linux的PCB结构体是task_struct,这个定义位于include/linux/sched.h
Tips:如果想在linux源码里找东西,可以用https://elixir.bootlin.com/ 这个网站,左边选择版本,右边输入关键字,即可查询到
在这里插入图片描述
在linux使用vim打开这个文件,往下翻,看到这段注释
在这里插入图片描述
按照提示添加属性即可

修改fork

这部分的源码是在kernel/fork.c中的copy_process函数里面
阅读源码,在合适的地方插入初始化hide属性的代码即可,这里我选择的位置是复制完进程信息之后,即下图所示的位置
在这里插入图片描述

添加系统调用

这部分我是看网上的各种文章,东拼西凑,进行多次实验之后才跑通的,事后想想,我应该最先去看linux kernel的官方手册
https://docs.kernel.org/process/adding-syscalls.html (附上官方手册)
以下是我自己添加系统调用的过程,这里用添加一个输出Hello World的简单系统调用来举例子
首先找到kernel/sys.c,在文件末尾使用SYSCALL_DEFINE宏来定义系统调用的函数体

SYSCALL_DEFINE0(hello)
{printk("hello world.114514\n");return 0;
}

解释一下,SYSCALL_DEFINE0(hello)表示定义一个含有0个参数的系统调用,名字是hello,通过查看sys.c里面其它函数的定义代码可以得知,如果想要添加一个只有一个参数的系统调用,那么应该使用SYSCALL_DEFINE1(hide,pid_t,pid),其中hide是系统调用的名字,pid_t是第一个参数类型,pid是第一个参数的名字,2个参数的同理
printk是输出日志,这个日志可以在sudo dmesg里面看到,printk支持使用%d,%s等对输出进行格式化,用法类似于printf

接下来修改系统调用表,在arch/x86/entry/syscalls/syscall_64.tbl中合适的地方添加刚才写的系统调用,这里我是添加在了334号系统调用之后的
在这里插入图片描述
仿照上面334写即可,其中hello是自己随便起的名字,而sys_hello是系统调用的函数名,这个函数名是上面SYSCALL_DEFINE0里写的函数名前面加上前缀sys_得到的

添加完成后可以尝试编译运行一下新的内核
可以使用uname -a查看当前内核是不是最新编译的(看时间即可)
在这里插入图片描述
下面将使用一段代码来测试一下新添加的335号系统调用

#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>int main(int argc,char **argv)
{printf("System call return %ld\n",syscall(335));return 0;
}

运行程序
在这里插入图片描述
然后使用sudo dmesg查看
在这里插入图片描述

编写hide系统调用

hide系统调用的实现有2个思路,一个是遍历所有进程,找到pid相符的进程,然后设置hide,另一个思路是通过pid找到进程,然后直接设置,这里采取后者
通过查找资料,可以得知,根据pid查找进程是用这段代码
pid_task(find_vpid(pid),PIDTYPE_PID);
最终完整的系统调用代码如下

SYSCALL_DEFINE2(hide,pid_t,pid,int,on)
{struct task_struct * me = NULL;me=pid_task(find_vpid(pid),PIDTYPE_PID);if(current->uid != 0){//User is not rootreturn 0;}if(me == NULL){return 0;}if( on == 1 ){me->hide = 1;}else{if( me->hide == 1 ){me->hide = 0;}}return 0;
}

接下来再修改系统调用表即可完成系统调用的添加

劫持获取所有进程的函数

现在已经可以通过系统调用来设置PCB里面的hide,下一步就是修改列举所有进程的函数,让它在列举时判断一下,如果hide==1就不列举

proc文件系统

在劫持之前,需要简单介绍一下proc文件系统。在linux根目录下,有一个/proc文件夹,这其实并不是在磁盘上真实存在的文件,而是一个虚拟文件系统。
proc文件夹里面有很多个以pid为名字的文件夹,这些文件夹里面又有若干个文件,读取这些文件就可以获取这个进程的相关信息,例如想查看pid为1的程序的名字可以使用sudo cat /proc/1/comm
这一系列操作在系统底层的实现是:系统在启动的时候就挂载了一个proc虚拟文件系统,当用户访问proc文件夹下的文件时,系统会调用proc文件系统里面相关的函数,而不是常规文件系统的函数,例如在执行ls /proc时,实际上系统会调用位于s/proc/base.c里面的proc_pid_readdir函数,这个函数会获取当前系统中所有的进程,随后会有函数把这个函数的返回值写入到读取文件操作的缓冲区中

修改代码

所以,我们的突破口就是proc_pid_readdir函数,在阅读这个函数的代码之后,可以找到突破口是一个put_task_struct函数的调用,如下图
在这里插入图片描述
那么只需要在这个if里面加上一个条件,即必须这个进程不被隐藏才能put,即可完成劫持

结果验证

在修改完源代码之后,重新编译和安装内核,启动新的内核
使用下面这段代码来测试

#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>int main(int argc,char **argv)
{int pid;int hide;scanf("%d %d",&pid,&hide);printf("System call return %ld\n",syscall(336,pid,hide));return 0;
}

编译运行程序
在这里插入图片描述
从图中可以看出,进程顺利隐藏,并且能够重新展示,要求一顺利实现

完成要求二

要求二是在要求一的基础上进行一些简单的扩展,这里可以使用一个比较暴力的思路,就是遍历所有进程,然后挨个判断uid和进程名称,把符合要求的进程的hide设置为1即可
这里只有三点需要注意一下
1.遍历所有进程可以使用for_each_process这个宏来完成,这个宏有类似于for循环的作用,用法如下

struct task_struct* p;
for_each_process(p){//Do something.....
}

这个宏的定义在include/linux/sched/signal.h里面,定义如下

#define for_each_process(p) \for (p = &init_task ; (p = next_task(p)) != &init_task ; )

2.用户态的字符串不能在内核态直接使用,需要调用strncpy_from_user把用户态的字符串复制到内核态的缓冲区才能使用,方法如下

char tmp_buf[256];
if(binname != NULL)strncpy_from_user(tmp_buf,binname,256);

最终的系统调用代码如下

SYSCALL_DEFINE2(hide_user_processes,uid_t,uid,char*,binname)
{uid_t curr_uid=current->uid;if(curr_uid != 0){//User is not rootreturn 0;}char tmp_buf[256];if(binname != NULL)strncpy_from_user(tmp_buf,binname,256);struct task_struct* p=NULL;for_each_process(p){if(p->real_cred->uid.val == uid){if(binname == NULL){p->hide=1;}else{char* s=p->comm;int identical=1;int i=0;for(i=0;tmp_buf[i]!='\0' && s[i] != '\0';i++){if(tmp_buf[i] != s[i]){identical=0;break;}}if(tmp_buf[i] != s[i])identical=0;if(identical == 1){p->hide=1;}}}}return 0;
}

在编译和安装完成之后可以写一段测试代码来验证一下代码的正确性

#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>int main(int argc,char **argv)
{int uid;char binname[20];scanf("%d %s",&uid,binname);printf("%s\n",binname);bool noBinname=false;if(strcmp(binname,"no") == 0){printf("Bin name set null\n");noBinname=true;}printf("System call return %ld\n",syscall(337,uid,noBinname?NULL:binname));return 0;
}

编译运行该程序
在这里插入图片描述
要求二完成

完成要求三

思路分析

之前提到过的,proc文件系统是一个虚拟文件系统,读取和写入proc文件夹下的文件的操作会交给一些特定的内核函数来执行,那么我们只需要添加一个/proc/hide条目,并配置这个条目的write函数,当write被调用的时候就根据写入的值设置一个全局变量,然后再修改proc_pid_readdir函数,添加一个判断,如果这个全局变量为0就不隐藏任何进程,这样就可以达到设置全局开关的目的

全局变量的定义和使用

全局变量可以跨文件被使用,在需要使用全局变量的地方使用extern关键字声明全局变量即可
需要注意的是,全局变量需要进行一次初始化,并且仅可以进行一次初始化
具体而言,可以这样操作:在需要使用全局变量hidden_flag的c文件里面使用下面这条语句进行声明

extern int hidden_flag;

然后在某个c文件中对hidden_flag变量进行定义

extern int hidden_flag;
int hidden_flag=1;

注意:声明是告诉编译器我这里有一个名叫hidden_flag的变量,我接下来会用这个变量,这个变量具体在哪需要编译器自己去找;而定义则是告诉编译器我新建了一个名为hidden_flag的变量,相当于真正为这个变量分配了内存空间

添加proc条目

大体流程

通过查阅资料和反复实验,我找到了在5.15.60版本添加proc条目的方法
proc文件系统的初始化函数在fs/proc/root.c里面,名叫proc_root_init
网上很多教材是要修改一个名叫proc_misc_init函数,但是在这个版本的内核源码里面找不到这个函数,所以索性就在proc_root_init函数里面添加条目了(因为看网上的代码,root_init是会调用misc_init的,所以猜测直接在root_init里面添加应该也是可以的,最后实践证明确实可行)

添加proc条目需要调用proc_create函数,该函数的定义如下:

struct proc_dir_entry *proc_create(const char *name, umode_t mode, struct proc_dir_entry *parent, const struct proc_ops *proc_ops);

可以看到,这个函数需要4个参数,第一个是文件名,这里要创建一个/proc/hidden,所以这个参数传hidden;第二个参数是权限,为了防止后续因为权限问题导致实验翻车,这里就给666了;第三个是parent,传NULL即可;第四个是这个条目操作的配置项的指针,可以在这里配置该条目的read和write函数

下面开始添加proc条目
首先要实现该条目的read和write函数,当用户态程序读取和写入/proc/hidden时,这两个函数就会被调用

read函数

下面是read函数的定义

ssize_t hidden_read_proc(struct file *filp,char *buf,size_t count,loff_t *offp);

第一个参数是文件;第二个参数是用户态的读取缓冲区,我们需要往这个缓冲区里面写数据来完成读取操作;第三个参数是这个用户态缓冲区的大小;第四个参数是上一次读取的位置,因为可能出现缓冲区不够等情况,用户态程序在读文件时通常是用下面的方式进行多次读取的

char buf[256];
int len;
while((len=read(buf))!=0){//此时buf中读取了len字节的数据,进行相应处理
}

所以read函数要做的事情就是往缓冲区中写入数据,修改offp,然后返回已经读入的字节数,下面是/proc/hidden条目的read函数的实现

ssize_t hidden_read_proc(struct file *filp,char *buf,size_t count,loff_t *offp )
{if(*offp > 0)return 0;char msg[256];int len=sprintf(msg,"Current flag is %d\n",hidden_flag);copy_to_user(buf,msg,len);*offp=len;return len;
}

需要注意的是,如果,没有最开始判断offp这一行,那么会出现读取/proc/hidden文件读不完的情况,具体而言,如果使用指令cat /proc/hidden,那么它会一直源源不断地蹦出字符,不会停,这是因为read函数始终不会返回0,导致那个while循环不会停
此外,同样的,内核态的内存和用户态的内存是不互通的,需要使用copy_to_user函数来完成内存的拷贝

write函数

write函数的定义如下

ssize_t hidden_write_proc(struct file *filp,const char *buf,size_t count,loff_t *offp);

参数的意义和read是类似的,第二个参数是用户即将写入的数据缓冲区地址,第三个则是数据量,以下是write函数的具体实现

ssize_t hidden_write_proc(struct file *filp,const char *buf,size_t count,loff_t *offp)
{char msg[2056];copy_from_user(msg,buf,count);hidden_flag=msg[0]-'0';return count;
}

需要注意的是,同样的,需要进行从用户态到内核态的内存拷贝

proc_ops结构体

接下来新建一个proc_ops结构体的对象,传入我们写的read和write函数

struct proc_ops hidden_proc_fops = {proc_read:  hidden_read_proc,proc_write: hidden_write_proc
};

当然,这个结构体还支持我们配置更多的内容,具体可以看这个结构体的定义,这里不再赘述了

调用proc_create

最后调用proc_create,传入参数,即可完成条目的创建

proc_create("hidden",666,NULL,&hidden_proc_fops);

结果验证

我们重新编译安装内核,然后隐藏一个进程,然后再向/proc/hidden里面写入0
在这里插入图片描述
可见,hidden_flag起效果了,要求三完成

完成要求四

有了要求三的铺垫,要求四就显得比较简单了,只需要实现一个read函数,在其中遍历所有进程,把hide为1的进程pid返回即可
read函数的实现如下

ssize_t pid_read_proc(struct file *filp,char *buf,size_t count,loff_t *offp ) 
{if(*offp > 0)return 0;char msg[1024];int len=0;struct task_struct* p;for_each_process(p){if(p->hide == 1){len += sprintf(msg+len,"%d ",p->pid);}}copy_to_user(buf,msg,len);*offp=len;return len;
}

可以把1000用户所有进程隐藏了,然后查看/proc/hidden_process文件来检查效果
在这里插入图片描述
可以在里面看到所有被隐藏的进程的pid,要求四完成

总结与心得

这次实验的代码量并不多,操作步骤也不复杂,主要的时间都花在了学习linux内核编程上面了。从零开始学习proc文件系统,linux源码,并建立临时知识体系,然后根据学到的东西进行开发实践,这是一个充满挑战性但也非常有意思的过程。在这过程中,我学到了linux内核编程的技术,跑通了从内核源码修改到最终运行的全流程,并对proc虚拟文件系统进行了更深入的自学,完成了四个实验要求。
个人感觉这过程中查资料自学的效率有点低,下次遇到此类问题应该首先查找官方的手册和教程,而不是在网上胡乱找相关的文章。
总的而言,收获很多,这是一次非常有意思的经历。
Anyway,写这篇博客也是记录一下这次实验的经历,感悟和收获,同时也为其他做这个实验的同学提供一点过来人的经验,希望能起到避坑的效果。

相关文章:

记一次linux操作系统实验

前言 最近完成了一个需要修改和编译linux内核源码的操作系统实验&#xff0c;个人感觉这个实验还是比较有意思的。这次实验总共耗时4天&#xff0c;从对linux实现零基础&#xff0c;通过查阅资料和不断尝试&#xff0c;直到完成实验目标&#xff0c;在这过程中确实也收获颇丰&…...

java操作富文本插入到word模板

最近项目有个需求&#xff0c;大致流程是前端保存富文本&#xff08;html的代码&#xff09;到数据库&#xff0c;后台需要将富文本代码转成带格式的文字&#xff0c;插入到word模板里&#xff0c;然后将word转成pdf&#xff0c;再由前端调用接口下载pdf文件&#xff01; 1、思…...

JMeter---BeanShell实现接口前置和后置操作

在JMeter中&#xff0c;可以使用BeanShell脚本来实现接口的前置和后置操作。 下面是使用BeanShell脚本实现接口前置和后置操作的步骤&#xff1a; 1、在测试计划中添加一个BeanShell前置处理器或后置处理器。 右键点击需要添加前置或后置操作的接口请求&#xff0c;选择&quo…...

【Linux】共享内存

文章目录 一、共享内存的原理详谈共享内存的实现过程二、共享内存的接口函数1.shmget2. shmatshmdtshmctl 进程间使用共享内存通信三、共享内存的特性 关于代码 一、共享内存的原理 共享内存是由操作系统维护和管理的一块内存。 共享内存的本质是内核级的缓冲区。 一个进程向…...

五、双向NAT

学习防火墙之前&#xff0c;对路由交换应要有一定的认识 双向NAT1.1.基本原理1.2.NAT Inbound NAT Server1.3.域内NATNAT Server —————————————————————————————————————————————————— 双向NAT 经过前面介绍&#xff0c;…...

P1028 [NOIP2001 普及组] 数的计算

时刻记住一句话&#xff1a;写递归&#xff0c;1画图&#xff0c;2大脑放空&#xff01;&#xff01;&#xff01; 意思是&#xff0c;自己写递归题目&#xff0c;先用样例给的数据画图&#xff0c;然后想一个超级简单的思路&#xff0c;直接套上去就可以了。 上题干&#xff…...

浅析三相异步电动机控制的电气保护

安科瑞 华楠 摘 要&#xff1a;要求三相异步电动机的控制系统不仅要保证电机正常启动和运行&#xff0c;完成制动操作&#xff0c;还要通过相关保护措施维护电动机的安全使用。基于此&#xff0c;本文以电动机电气保护作为研究对象&#xff0c;结合三相异步电动机的机械特点&…...

Java设计模式系列:单例设计模式

Java设计模式系列&#xff1a;单例设计模式 介绍 所谓类的单例设计模式&#xff0c;就是采取一定的方法保证在整个的软件系统中&#xff0c;对某个类只能存在一个对象实例&#xff0c;并且该类只提供一个取得其对象实例的方法&#xff08;静态方法&#xff09; 比如 Hiberna…...

开拓新天地:探讨数位行销对医药产业医病连结的影响

数字营销模式多元&#xff0c;主要围绕医生和患者。赛道各企业凭借各自优势&#xff08;技术、学术、流量等&#xff09;入局&#xff0c;提供各自差异化营销工具或服务。目前&#xff0c;围绕医生的数字营销旨在为医生提供全面学术解决方案从而提升对医药产品的认可&#xff0…...

[tsai.shen@mailfence.com].faust勒索病毒数据怎么处理|数据解密恢复

导言&#xff1a; [support2022cock.li].faust、[tsai.shenmailfence.com].faust、[Encrypteddmailfence.com].faust勒索病毒是一种具有恶意目的的勒索软件&#xff0c;其主要特点包括对受害者文件进行强力加密&#xff0c;然后勒索受害者支付赎金以获取解密密钥。攻击者通常通…...

Peter算法小课堂—前缀和数组的应用

桶 相当于计数排序&#xff0c;看一个视频 桶排序 太戈编程1620题 算法解析 #include <bits/stdc.h> using namespace std; const int R11; int cnt[R];//cnt[t]代表第t天新增几人 int s[R];//s[]数组是cnt[]数组的前缀和数组 int n,t; int main(){cin>>n;for(…...

线性表之链式表

文章目录 主要内容一.单链表1.头插法建立单链表代码如下&#xff08;示例&#xff09;: 2.尾插法建立单链表代码如下&#xff08;示例&#xff09;: 3.按序号查找结点值代码如下&#xff08;示例&#xff09;: 4.按值查找表结点代码如下&#xff08;示例&#xff09;: 5.插入节…...

[Docker]十.Docker Swarm讲解

一.Dokcer Swarm集群介绍 1.Dokcer Swarm 简介 Docker Swarm 是 Docker 公司推出的用来管理 docker 集群的工具&#xff0c; 使用 Docker Swarm 可以快速方便的实现 高可用集群 ,Docker Compose 只能编排单节点上的容器, Docker Swarm 可以让我们在单一主机上操作来完成对 整…...

相机机模组需求示例

产品需求名称摄像头采集图片数据补充说明产品需求描述 As&#xff1a;用户 I want to&#xff1a;通过相机模组获取到自定义格式图片数据&#xff0c;要求包括&#xff1a; 1、支持多种场景&#xff0c;如&#xff1a;手持相机拍摄舌苔 2、支持图片分辨率至少达到1920X1080 3、…...

Uniapp 微信登录流程解析

本文将介绍在 Uniapp 应用中实现微信登录的流程&#xff0c;包括准备工作、授权登录、获取用户信息等步骤。 内容大纲&#xff1a; 介绍Uniapp和微信登录&#xff1a; 简要介绍 Uniapp 框架以及微信登录的重要性和流行程度。 准备工作&#xff1a; 注册微信开发者账号创建应用…...

红旗Asianux Server Linux V8 安装万里数据库(GreatSQL)

红旗Asianux Server Linux V8 安装万里数据库&#xff08;GreatSQL&#xff09; 红旗Asianux介绍&#xff1a; 红旗Asianux Server Linux 8.0是为云时代重新设计的操作系统&#xff0c;为云时代的到来引入了大量新功能&#xff0c;包括用于配置管理、快速迁移框架、编程语言和…...

一文2000字使用JMeter进行接口测试教程!(建议收藏)

安装 使用JMeter的前提需要安装JDK&#xff0c;需要JDK1.7以上版本目前在用的是JMeter5.2版本&#xff0c;大家可自行下载解压使用 运行 进入解压路径如E: \apache-jmeter-5.2\bin&#xff0c;双击jmeter.bat启动运行 启动后默认为英文版本&#xff0c;可通过Options – Cho…...

Spark---介绍及安装

一、Spark介绍 1、什么是Spark Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎。Spark是UC Berkeley AMP lab (加州大学伯克利分校的AMP实验室)所开源的类Hadoop MapReduce的通用并行计算框架&#xff0c;Spark拥有Hadoop MapReduce所具有的优点&#xff1b;但…...

uni-app:实现request请求的递归(设置request请求的访问次数),并且调用自定义方法给出返回值

一、效果展示 失败效果 成功效果 二、写入后端请求部分 分析 ①自定义一个模块common.js主要用于封装所有的请求函数 ②核心代码 function requestWithRetry(cmd, username, password, retryCount) {return new Promise((resolve, reject) > {uni.request({url: ip sys…...

数据结构-归并排序+计数排序

1.归并排序 基本思想&#xff1a; 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并&#xff0c;得到完全有序的序列&#xff1b;即先使每个子序列有序&#xff0c;再使子序列段间有序。若将两个有序表合并成一个…...

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析

今天聊的内容&#xff0c;我认为是AI开发里面非常重要的内容。它在AI开发里无处不在&#xff0c;当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗"&#xff0c;或者让翻译模型 "将这段合同翻译成商务日语" 时&#xff0c;输入的这句话就是 Prompt。…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

基于Docker Compose部署Java微服务项目

一. 创建根项目 根项目&#xff08;父项目&#xff09;主要用于依赖管理 一些需要注意的点&#xff1a; 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件&#xff0c;否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

【Oracle】分区表

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...

大数据学习(132)-HIve数据分析

​​​​&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4…...

技术栈RabbitMq的介绍和使用

目录 1. 什么是消息队列&#xff1f;2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...

R语言速释制剂QBD解决方案之三

本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...

【Linux】Linux 系统默认的目录及作用说明

博主介绍&#xff1a;✌全网粉丝23W&#xff0c;CSDN博客专家、Java领域优质创作者&#xff0c;掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围&#xff1a;SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...

STM32---外部32.768K晶振(LSE)无法起振问题

晶振是否起振主要就检查两个1、晶振与MCU是否兼容&#xff1b;2、晶振的负载电容是否匹配 目录 一、判断晶振与MCU是否兼容 二、判断负载电容是否匹配 1. 晶振负载电容&#xff08;CL&#xff09;与匹配电容&#xff08;CL1、CL2&#xff09;的关系 2. 如何选择 CL1 和 CL…...