记一次linux操作系统实验

news2024/11/15 4:20:31

前言

最近完成了一个需要修改和编译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 root
		return 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 root
		return 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,写这篇博客也是记录一下这次实验的经历,感悟和收获,同时也为其他做这个实验的同学提供一点过来人的经验,希望能起到避坑的效果。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

井盖位移监测系统怎么监测井盖位移

党的二十大报告提出&#xff0c;坚持人民城市人民建、人民城市为人民&#xff0c;提高城市规划、建设、治理水平。秉持依法治理、创新引领的理念&#xff0c;市政府应该坚定推进窨井盖安全管理工作&#xff0c;不断加大排查整治力度&#xff0c;弥补设施安全管理短板&#xff0…

每日一练:“打家劫舍“(House Robber)问题 II

有想要了解打家劫舍初级问题的&#xff0c;可以点击下面链接查看&#xff01; 每日一练&#xff1a;“打家劫舍“&#xff08;House Robber&#xff09;问题 I 1. 问题 假设有房屋形成一个环形&#xff0c;即第一个房屋和最后一个房屋也相邻&#xff0c;每个房屋里都存放着一定…

Java数组的复制、截取(内含例题:力扣-189.轮转数组)

目录 数组的复制、截取&#xff1a; 1、使用Arrays中的copyOf方法完成数组的拷贝 2、使用Arrays中的copyofRange方法完成数组的拷贝 题目链接&#xff1a; 数组的复制、截取&#xff1a; 1、使用Arrays中的copyOf方法完成数组的拷贝 public class Csdn {public static vo…

【Git】一文教你学会 submodule 的增、查、改、删

添加子模块 $ git submodule add <url> <path>url 为想要添加的子模块路径path 为子模块存放的本地路径 示例&#xff0c;添加 r-tinymaix 为子模块到主仓库 ./sdk/packages/online-packages/r-tinymaix 路径下&#xff0c;命令如下所示&#xff1a; $ git subm…

UI自动化测试神器:RunnerGo测试平台

可以直接进入官网下载开源版或点击右上角体验企业版体验 RunnerGo UI自动化平台 RunnerGo提供从API管理到API性能再到可视化的API自动化、UI自动化测试功能模块&#xff0c;覆盖了整个产品测试周期。 RunnerGo UI自动化基于Selenium浏览器自动化方案构建&#xff0c;内嵌高度…

可持续创新 精选路线

在加速企业数字化转型、 实现智能制造的升级之路上&#xff01; 使用好的工具固然重要&#xff0c; 而有好工具&#xff0c;也要会用工具。生信科技不仅为企业提供强大的产品支持&#xff0c; 更有全方位的定制化服务&#xff0c; 提升工程师的工具应用能力&#xff0c; 让企业…

海外https代理ip如何保障信息安全?该怎么选择?

海外https代理ip是指通信协议为https的海外真实网络地址ip&#xff0c;通常应用在各种跨境业务中。 一、什么是HTTPS协议 HTTP协议是一个应用层协议&#xff0c;通常运行在TCP协议之上。它是一个明文协议&#xff0c;客户端发起请求&#xff0c;服务端给出响应的响应。由于网…

pat实现基于邻接表表示的深度优先遍历[含非递归写法]

文章目录 1.递归2.非递归 1.递归 void DFS(ALGraph G, int v) {visited[v] 1;printf("%c ", G.vertices[v].data);for (ArcNode* cur G.vertices[v].firstarc; cur ! nullptr; cur cur->nextarc){if (!visited[cur->adjvex])DFS(G, cur->adjvex);} }2.非…

matlab画双坐标图的样式

matlab画双坐标图的样式 %% clc,clear,close all; t0:0.1:9*pi; figure; [AX,Ha,Hb]plotyy(t,sin(t),t,exp(t)); % 绘图并创建句柄 % ----------------- 设置刻度 set(AX(1),yTick,[-1.250:0.25:1.25]) % 设置左边Y轴的刻度 set(AX(2),yTick,[0:50:350]) …

2023年【危险化学品经营单位安全管理人员】考试内容及危险化学品经营单位安全管理人员最新解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品经营单位安全管理人员考试内容是安全生产模拟考试一点通生成的&#xff0c;危险化学品经营单位安全管理人员证模拟考试题库是根据危险化学品经营单位安全管理人员最新版教材汇编出危险化学品经营单位安全管…

HarmonyOS开发:ArkTs常见数据类型

前言 无论是Android还是iOS开发&#xff0c;都提供了多种数据类型用于常见的业务开发&#xff0c;但在ArkTs中&#xff0c;数据类型就大有不同&#xff0c;比如int&#xff0c;float&#xff0c;double&#xff0c;long统一就是number类型&#xff0c;当然了也不存在char类型&…

新式的拉式膜片弹簧离合器设计机械设计CAD

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;离合器 获取完整论文报告工程源文件 减震弹簧 摩擦片 膜片弹簧 压盘 轴 扭转减震器 从动盘 离合器 离合器的结构设计 为了达到计划书所给的数据要求&#xff0c;设计时应根据车型的类别、使用要求、制造条件&#xff0c;…

Flink-简介与基础

Flink-简介与基础 一、Flink起源二、Flink数据处理模式1.批处理2.流处理3.Flink流批一体处理 三、Flink架构1.Flink集群2.Flink Program3.JobManager4.TaskManager 四、Flink应用程序五、Flink高级特性1.时间流&#xff08;Time&#xff09;和窗口&#xff08;Window&#xff0…

FreeRTOS深入教程(信号量源码分析)

文章目录 前言一.创建信号量二.释放信号量三.获取信号量成功获取获取不成功 总结 前言 本篇文章将为大家讲解信号量&#xff0c;源码分析。 在 FreeRTOS 中&#xff0c;信号量的实现基于队列。这种设计的思想是利用队列的特性来实现信号量&#xff0c;因为信号量可以被视为只…

借助 XEOS V6, 农牧龙头企业实现原有存储的高效在线替换

面对旧有存储系统的应用不足&#xff0c;某大型现代农牧龙头企业采用了星辰天合的对象存储 XEOS V6 方案&#xff0c; 该方案以其卓越的技术架构和同城双活异地灾备的解决方案完整性&#xff0c;在无缝高效完成系统替换的同时&#xff0c;可以极大地提升系统的灵活性和业务的连…

C/C++ 实现Socket交互式服务端

在 Windows 操作系统中&#xff0c;原生提供了强大的网络编程支持&#xff0c;允许开发者使用 Socket API 进行网络通信&#xff0c;通过 Socket API&#xff0c;开发者可以创建、连接、发送和接收数据&#xff0c;实现网络通信。本文将深入探讨如何通过调用原生网络 API 实现同…

RabbitMQ之发送者(生产者)可靠性

文章目录 前言一、生产者重试机制二、生产者确认机制实现生产者确认&#xff08;1&#xff09;定义ReturnCallback&#xff08;2&#xff09;定义ConfirmCallback 总结 前言 生产者重试机制、生产者确认机制。 一、生产者重试机制 问题&#xff1a;生产者发送消息时&#xff0…

【python VS vba】(6) python的常用函数print()的各种用法和细节(未完成)

1 基本语法 print() 3.0 都是函数用法 2 可打印的各种对象 直接打印对象 直接打印数组 直接打印string 数值等 3 打印的各种内容如何连接 逗号分隔 &不行 3 转义符 可用转义符 换行的特殊符号 /n 4 print() 里带变量怎么输出 试验了4种输出带变量的方法&#xf…

无需外接显示器,直接使用windows安装树莓派系统并可远程桌面登录

准备工作: 1.安装树莓派官方烧录工具 raspberry pi imager 2.下载树莓派系统镜像(也可选择在线下载安装) 打开imager工具&#xff0c;选择需要安装包树莓派版本 点击"NEXT"&#xff0c;在弹出的选项中选择编辑设置。 设置登录名和密码&#xff0c;已经所连接的wif…

如何有效减少 AI 模型的数据中心能源消耗?

在让人工智能变得更好的竞赛中&#xff0c;麻省理工学院&#xff08;MIT&#xff09;林肯实验室正在开发降低功耗、高效训练和透明能源使用的方法。 在 Google 上搜索航班时&#xff0c;您可能已经注意到&#xff0c;现在每个航班的碳排放量估算值都显示在其成本旁边。这是一种…