Linux Namespace

Linux namespaces 介绍

namespaces是Linux内核用来隔离内核资源的方式。通过namespaces可以让一些进程只能看到与自己相关的那部分资源。而其它的进程也只能看到与他们自己相关的资源。这两拨进程根本感知不到对方的存在。而它具体的实现细节是通过Linux namespaces来实现的。

总结: Linux namespaces对系统进程进行轻量的虚拟化隔离。

当前Linux内核只支持6中namespaces:

● mnt(mount points, filesystems)

● pid(process)

● net(network stack)

● ipc(System V IPC)

● uts(hostname)

● user(UIDs)

下面是Linux Kernel版本迭代过程中对这6中namespaces的支持情况及对应的flag:

最初打算对Linux内核支持10种namespaces,但是下面的4中没有实现:

● security namespace

● security keys namespaces

● device namespace

● time namespace

接下来先介绍namespace的API,然后在针对Linux内核现在支持的6中namespace分别进行介绍。

代码测试环境:ubuntu20.04.2,kernel版本:5.4.0-182-generic

Namespaces API 介绍

下面3个系统调用API会被用于namespaces:

● clone(): 用于创建新的进程同时创建新的namespaces。并且新的进程会被attach到新的namespace里面。

int clone(int (*fn)(void *), void *child_stack,
       int flags, void *arg, .../* pid_t *ptid, void *newtls, pid_t *ctid */ );

● 参数child_func传入子进程运行的程序主函数。

● 参数child_stack传入子进程使用的栈空间

● 参数flags表示使用哪些CLONE_*标志位

● 参数args则可用于传入用户参数

ClONE_NEW* flag有20多被包含在include/linux/sched.h头文件中。

● unshare(): 不会创建新的进程,但是会创建新的namesapce并把当前的进程attach到该namespace里面。

int unshare(int flags);

● setns(): 将进程attach到一个已经存在的namespace里面。

int setns(int fd, int nstype);

● 参数fd表示我们要加入的namespace的文件描述符。如:/proc/[pid]/ns下面对应的文件描述符。

● 参数nstype让调用者可以去检查fd指向的namespace类型是否符合我们实际的要求。如果填0表示不检查。

namespace 实践

为了最好的体验还是在 Linux 内核 3.8 以上的系统上进行(这里使用的 Ubuntu 20.04.2kernel版本:5.4.0-182-generic)。为什么不用 docker for windows 或者 docker for mac 呢?因为这两个其实还是是在 linux 虚拟机上运行 docker 的,docker for windows 需要将 linux 虚拟机装在开启 hyper-v 的 win10 专业版上,而 docker for mac 使用通过 HyperKit 运行 linux 虚拟机。为了方便,使用 c语言代码 来演示循序渐进的达到 Docker 的体验。

UTS Namespace

UTS namespace提供了主机名和域名的隔离,这样每一个容器就可以拥有独立的主机名和域名,在网络上可以被视为一个独立的节点而非宿主机上的一个进程。

下面让我们来看下UTS的隔离效果,测试代码如下:

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>#define STACK_SIZE (1024*1024)static char child_stack[STACK_SIZE];
char* const child_args[] = {"/bin/bash",
	NULL
};int child_main(void* args){
	printf("in child process!\n");
	sethostname("changed namespace", 12);
	execv(child_args[0], child_args);return 1;
}int main(){
	printf("program begin: \n");
	int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD|CLONE_NEWUTS, NULL);
	waitpid(child_pid, NULL, 0);
	printf("quit\n");return 0;
}

主要是main中在调用clone函数创建新进程及新namespace的时候,传递了CLONE_NEWUTS flag,用于对主机名和域名的隔离。

如果没有gcc需要先安装gcc环境

编译并运行程序会发现主机名发生了变化。

root@double:~# gcc -Wall uts.c -o uts && ./uts
program begin: 
in child process!
root@changed name:~# hostname
changed name
root@changed name:~# exit
exit
quit

每个容器的主机名不同就是使用UTS Namespace机制实现的。

IPC Namespace

容器中进程间的通信采用的方式包括: 信号量消息队列共享内存。与虚拟机不同的是,容器内部进程间通信对宿主机来说,实际上是具有相同的PID namespace中的进程间通信,因此需要一个而唯一的标识符来进行区别。申请IPC资源就申请了这样一个全局唯一的32位ID,所以IPC namespace中实际上包含了系统IPC标识符以及实现POSIX消息队列的文件系统。在同一个IPC namespace下的进程彼此可见,而与其他的IPC namespace下的进程则互相不可见。

下面我们来看下IPC的隔离效果,测试代码如下:

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>#define STACK_SIZE (1024*1024)static char child_stack[STACK_SIZE];
char* const child_args[] = {
	"/bin/bash",
	NULL
};int child_main(void* args){
	printf("in child process!\n");
	sethostname("changed namespace", 12);
	execv(child_args[0], child_args);
	return 1;
}int main(){
	printf("program begin: \n");
	int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC, NULL);
	waitpid(child_pid, NULL, 0);
	printf("quit\n");
	return 0;
}

main函数中调用clone函数创建新进程同时创建新namespaces的时候,传递CLONE_NEWIPCflag, 来实现进程间IPC的隔离。

在运行程序的时候,为了方便测试进程间通信是否被真正的隔离了,

1.  首先我们先使用ipcmk -Q命令创建一个queue:

root@double:~# ipcmk -Q
Message queue id: 0

2.  使用ipcs -q查看queue是否创建成功:

root@double:~# ipcs -q------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0xe594be1e 0          root       644        0            0           

3.  编译并运行ipc.c代码对IPC进行隔离并进行验证:

root@double:~# gcc -Wall ipc.c -o ipc && ./ipc
program begin: 
in child process!
root@changed name:~# ipcs -q------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

从运行的结果来看,已经找不到原先声明的message queue,实现了IPC的隔离。

PID Namespace

PID namespace隔离非常实用,它对进程PID重新标号,即两个不同的namespace下的进程可以拥有同一个PID。每一个PID namespace都有字的计数程序。内核为所有的PID namespace维护了一个树状结构,最顶层的是系统初始时创建的,即root namespace。他创建的新的PID namespace称child namespace。通过这种方式,不同的PID namespace会形成一个等级的体系。所属的父节点可以看到子节点中的进程,并可以通过信号等方式对子节点中的进程产生影响。反过来,子节点不能看到父节点PID namespace中的任何内容。

由此产生如下结论:

● 每个PID namespace中的第一个进程“PID 1“,都会像传统Linux中的init进程一样拥有特权,起特殊作用。

● 一个namespace中的进程,不可能通过kill或ptrace影响父节点或者兄弟节点中的进程,因为其他节点的PID在这个namespace中没有任何意义。

● 如果你在新的PID namespace中重新挂载/proc文件系统,会发现其下只显示同属一个PID namespace中的其他进程。

● 在root namespace中可以看到所有的进程,并且递归包含所有子节点中的进程。

● 

下面我们来看下对PID的隔离效果,测试代码如下:

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>#define STACK_SIZE (1024*1024)static char child_stack[STACK_SIZE];
char* const child_args[] = {
	"/bin/bash",
	NULL
};int child_main(void* args){
	printf("in child process!\n");
	sethostname("changed namespace", 12);
	execv(child_args[0], child_args);
	return 1;
}int main(){
	printf("program begin: \n");
	int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID, NULL);
	waitpid(child_pid, NULL, 0);
	printf("quit\n");
	return 0;
}

main函数中调用clone函数创建新的进程同时创建新的namespace的时候,传递CLONE_NEWPID flag。来实现对PIDg隔离。

让我们编译并运行代码,看下效果:

root@double:~# vi pid.c
root@double:~# gcc -Wall pid.c -o pid && ./pid
program begin: 
in child process!
root@changed name:~# echo $$
1
root@changed name:~# ps aux

我们可以看到,子进程的pid是1了,但是,我们会发现,在子进程的shell里输入ps,top等命令,我们还是可以看得到所有进程。说明并没有完全隔离。这是因为,像ps, top这些命令会去读/proc文件系统,所以,因为/proc文件系统在父进程和子进程都是一样的,所以这些命令显示的东西都是一样的。

所以,我们还需要对文件系统进行隔离。

Mount Namespace

Mount namespace通过隔离文件系统挂载点对隔离文件系统提供支持。隔离后,不同的mount namespace中的文件结构发生变化也互不影响。你可以通过/proc/[pid]/mounts查看到所有挂载在当前namesapce中的文件系统,还可以通过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载的文件名称,文件系统类型,挂载位置等等。

进程在创建mount namespace的时候,会把当前结构复制给新的namespace。 新的namespace中的所有mount操作都影响自身的文件系统,而对外界不会产生任何影响。这样做就严格地实现了隔离。

让我们来对文件系统进行隔离,测试的代码如下:

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>#define STACK_SIZE (1024 * 1024)
// sync primitive
int checkpoint[2];
static char child_stack[STACK_SIZE];
char* const child_args[] = {
	"/bin/bash",
	NULL
};int child_main(void* arg) {
	char c;
	// init sync primitive
	close(checkpoint[1]);
	// setup hostname
	sethostname("changed namespace", 12);
	// remount "/proc" to get accurate "top" && "ps" output
	mount("proc", "/proc", "proc", 0, NULL);
	// wait...
	read(checkpoint[0], &c, 1);
	execv(child_args[0], child_args);
	printf("Ooops\n");
	return 1;
}
int main() {
	// init sync primitive
	pipe(checkpoint);
	int child_pid = clone(child_main, child_stack+STACK_SIZE,
	  CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
	// further init here (nothing yet)
	// signal "done"
	close(checkpoint[1]);
	waitpid(child_pid, NULL, 0);
	printf("quit!\n");
	return 0;
}

main函数中调用clone函数创建新进程同时创建新的namespace时,需要增加CLONE_NEWNS flag。来实现对Mount namespace的隔离。

下面让我们编译并运行下程序来验证是否实现了对文件系统的隔离。

root@double:~# gcc -Wall mntns.c -o mnt && ./mnt
root@changed name:~# ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.2   9836  3952 pts/2    S    18:08   0:00 /bin/bash
root           8  0.0  0.1  11488  3288 pts/2    R+   18:08   0:00 ps aux

上面,我们可以看到只有两个进程 ,而且pid=1的进程是我们的/bin/bash。我们还可以看到/proc目录下也干净了很多:

root@changed name:~# ls /proc
1          bus       cpuinfo    dma          filesystems  ioports   keys         kpagecount  mdstat   mounts        partitions   scsi      stat           sysvipc      uptime             vmstat
9          cgroups   crypto     driver       fs           irq       key-users    kpageflags  meminfo  mtrr          pressure     self      swaps          thread-self  version            zoneinfo
acpi       cmdline   devices    execdomains  interrupts   kallsyms  kmsg         loadavg     misc     net           sched_debug  slabinfo  sys            timer_list   version_signature
buddyinfo  consoles  diskstats  fb           iomem        kcore     kpagecgroup  locks       modules  pagetypeinfo  schedstat    softirqs  sysrq-trigger  tty          vmallocinfo
关于mount相关的知识很多。这里就具体的详细介绍了,如果感兴趣可以看下:

Mount namespaces and shared subtrees

mount a filesystem

User Namespace

注意:User namespace是Linux内核(Linux 3.8)最后支持的namespace,所以有的版本的系统内核可能还没有对该namespace支持。

User namespace主要隔离了安全相关的标识符和属性,包括用户ID,用户组ID,root目录等。通俗点就是: 一个普通用户的进程通过clone()创建新的进程在新user namespace中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特殊权限的普通用户,但是它创建的容器进程却属于拥有所有权限的超级用户,这个技术为容器提供了极大的自由。

Linux中,特权用户的user ID是0,演示的最终我们将看到user ID非0的进程启动user namespace后user ID可以变为0。使用user namespace的方法和其它的namespace的使用方式没有太大的区别。即调用clone()的时候,需要加入CLONE_NEWUSER标识位。

让我们来看下user namespace的隔离效果,测试代码如下:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/capability.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>#define STACK_SIZE (1024 * 1024)static char container_stack[STACK_SIZE];
char* const container_args[] = {
    "/bin/bash",
    NULL
};int pipefd[2];void set_map(char* file, int inside_id, int outside_id, int len) {
    FILE* mapfd = fopen(file, "w");
    if (NULL == mapfd) {
        perror("open file error");
        return;
    }
    fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    fclose(mapfd);
}void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/uid_map", pid);
    set_map(file, inside_id, outside_id, len);
}void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/gid_map", pid);
    set_map(file, inside_id, outside_id, len);
}int container_main(void* arg)
{    printf("Container [%5d] - inside the container!\n", getpid());    printf("Container: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());    /* 等待父进程通知后再往下执行(进程间的同步) */
    char ch;
    close(pipefd[1]);
    read(pipefd[0], &ch, 1);    printf("Container [%5d] - setup hostname!\n", getpid());
    //set hostname
    sethostname("container",10);    //remount "/proc" to make sure the "top" and "ps" show container's information
    mount("proc", "/proc", "proc", 0, NULL);    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}int main()
{
    const int gid=getgid(), uid=getuid();    printf("Parent: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());    pipe(pipefd);    printf("Parent [%5d] - start a container!\n", getpid());    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);    printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid);    //To map the uid/gid, 
    //   we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent
    //The file format is
    //   ID-inside-ns   ID-outside-ns   length
    //if no mapping, 
    //   the uid will be taken from /proc/sys/kernel/overflowuid
    //   the gid will be taken from /proc/sys/kernel/overflowgid
    set_uid_map(container_pid, 0, uid, 1);
    set_gid_map(container_pid, 0, gid, 1);    printf("Parent [%5d] - user/group mapping done!\n", getpid());    /* 通知子进程 */
    close(pipefd[1]);    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}

在编译并执行代码之前,我们先来看下当前的用户uid和gid.(需以普通用户执行)

$ id
uid=1000(double) gid=1000(double) groups=1000(double)

现在编译并运行我们的代码来验证user namespace是否隔离成功:

注意: 如果编译时如下报错:

$ gcc userns.c -Wall -lcap -o userns && ./userns
userns.c:7:10: fatal error: sys/capability.h: No such file or directory
    7 | #include <sys/capability.h>
      |          ^~~~~~~~~~~~~~~~~~
compilation terminated.

则在ubuntu编译则需要安装libcap-dev包,如果在centos上编译则需要安装libcap-devel包。

重新执行

$ gcc userns.c -Wall -lcap -o userns && ./userns
Parent: eUID = 1000;  eGID = 1000, UID=1000, GID=1000
Parent [ 9527] - start a container!
Parent [ 9527] - Container [ 9528]!
Parent [ 9527] - user/group mapping done!
Container [    1] - inside the container!
Container: eUID = 0;  eGID = 65534, UID=0, GID=65534
Container [    1] - setup hostname!

我们可以看到容器里的用户和命令行提示符是root用户了

root@container:~# id
uid=0(root) gid=65534(nogroup) groups=65534(nogroup)

Network Namespace

总结

容器的隔离实现基本就是通过Linux内核提供的这6种namespace实现。但是容器依旧没有实现完全的环境隔离。比如: SELinux,Cgroups以及/sys/proc/sys/dev/sd*等目录下的资源依据是没有被隔离的。因此我们通常使用的ps, top命令查看到的数据依旧是宿主机的数据。因为它们的数据来源于/proc等目录下的文件。如果想要在可视化的角度来实现这方便的可视化隔离。可以看看之前调研的lxcfs对docker容器隔离

参考

浅谈 Linux Namespace | xigang's home

Docker基础技术:Linux Namespace(下) | 酷 壳 - CoolShell

http://docs.wixstatic.com/ugd/295986_d73d8d6087ed430c34c21f90b0b607fd.pdf

http://ramirose.wixsite.com/ramirosen

Docker背后的内核知识——Namespace资源隔离_语言 & 开发_孙健波_InfoQ精选文章

Linux Namespace分析——mnt namespace的实现与应用

Linux内核的namespace机制分析 - kk Blog —— 通用基础

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

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

相关文章

(三)C++之运算符重载

一.概念 C准许以运算符命名函数&#xff01;&#xff01;&#xff01; string a “hello”; a “ world”;// (a, “world”); cout<<“hello”; // <<(cout, “hello”); 可重载的运算符 不可重载的运算符 二.成员函数式(第一个行参是对象的引用) class T…

orcad导出pdf 缺少title block

在OrCAD中导出PDF时没有Title Block 最后确认问题在这里&#xff1a; 要勾选上Title Block Visible下面的print

Nginx详解(超级详细)

目录 Nginx简介 1. 为什么使用Nginx 2. 安装Nginx Nginx的核心功能 1. Nginx反向代理功能 2. Nginx的负载均衡 3 Nginx动静分离 Nginx简介 Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器&#xff0c;在BSD-like 协…

你能分清工业领域这些常见的技术文档吗?

在制造业领域中&#xff0c;技术文档是不可或缺的宝贵资源。它们不仅是产品设计理念的载体&#xff0c;更是指导生产、保证质量、降低错误的关键。技术文档详尽描述了产品的每一个细节&#xff0c;从设计原理到零部件规格&#xff0c;从装配步骤到操作指南&#xff0c;无所不包…

关于dom4j主节点的xmlns无法写入的问题

由于最近需要做一个xml的文件&#xff0c;使用dom4j的时候发现了一个bug&#xff0c;就是我的xmlns根本无法写入到xml的头部标签中。 Element element document.addElement("test"); element.addAttribute("xmlns", "urn:Declaration:datamodel:sta…

Windows10 22H2专业工作站版:功能全新升级,工作更高效!

Windows10 22H2专业工作站版是一款专为具有高级数据需求的人士设计的操作系统&#xff0c;拥有强大的服务器级数据保护和性能&#xff0c;可以帮助用户不断突破高级工作负载的挑战。接下来系统之家小编给大家带来全新升级的Windows10 22H2专业工作站版系统&#xff0c;喜欢的用…

刚起步的家庭海外仓:涉及到的全部业务优化流程

对于家庭海外仓来说&#xff0c;最难的阶段应该就是刚起步的时候。对业务流程不熟悉&#xff0c;也没有客户积累&#xff0c;本身的预算又十分有限。 在这个情况下应该注意什么&#xff0c;怎样才能顺利的开展业务&#xff1f;今天我们就针对这个问题详细的梳理了一下家庭海外…

界面控件DevExpress Blazor UI v24.1 - 发布全新TreeList组件

DevExpress Blazor UI组件使用了C#为Blazor Server和Blazor WebAssembly创建高影响力的用户体验&#xff0c;这个UI自建库提供了一套全面的原生Blazor UI组件&#xff08;包括Pivot Grid、调度程序、图表、数据编辑器和报表等&#xff09;。 DevExpress Blazor控件目前已经升级…

电脑屏幕录制怎么弄?分享3个简单的电脑录屏方法

在信息爆炸的时代&#xff0c;屏幕上的每一个画面都可能成为我们生活中不可或缺的记忆。作为一名年轻男性&#xff0c;我对于录屏软件的需求可以说是既挑剔又实际。今天&#xff0c;我就为大家分享一下我近期体验的三款录屏软件&#xff1a;福昕录屏大师、转转大师录屏大师和OB…

高频面试题-CSS

BFC 介绍下BFC (块级格式化上下文) 1>什么是BFC BFC即块级格式化上下文&#xff0c;是CSS可视化渲染的一部分, 它是一块独立的渲染区域&#xff0c;只有属于同一个BFC的元素才会互相影响&#xff0c;且不会影响其它外部元素。 2>如何创建BFC 根元素&#xff0c;即HTM…

maven项目容器化运行之2-maven中使用docker插件调用远程docker构建服务并在1Panel中运行

一.背景 公司主机管理小组的同事期望我们开发的maven项目能够在1Panel管理的docker容器部署。上一篇写了先开放1Panel中docker镜像构建能力maven项目容器化运行之1-基于1Panel软件将docker镜像构建能力分享给局域网-CSDN博客。这一篇就是演示maven工程的镜像构建、容器运行、运…

昇思25天学习打卡营第14天|DCGAN 与漫画头像生成:原理剖析与训练实战

目录 数据集下载 数据处理 构建生成器 构建判别器 模型训练 结果展示 数据集下载 首先尝试卸载已安装的 mindspore 库&#xff0c;然后通过指定的镜像源安装特定版本&#xff08;2.2.14&#xff09;的 mindspore 库。从指定的 URL 下载一个 zip 文件到当前目录下的 ./faces…

SMTP服务器地址与端口号有哪些关系与区别?

SMTP服务器地址如何正确配置&#xff1f;怎么验证服务器的地址&#xff1f; 了解SMTP服务器地址与端口号的关系与区别对于确保邮件系统的正常运作至关重要。AokSend将详细探讨这两者之间的关系和区别&#xff0c;并解释它们在邮件传输过程中的重要性。 SMTP服务器地址&#x…

吴恩达深度学习笔记:机器学习策略(2)(ML Strategy (2)) 2.9-2.10

目录 第三门课 结构化机器学习项目&#xff08;Structuring Machine Learning Projects&#xff09;第二周&#xff1a;机器学习策略&#xff08;2&#xff09;(ML Strategy (2))2.9 什么是端到端的深度学习&#xff1f;&#xff08;What is end-to-end deep learning?&#x…

spring是如何解决循环依赖的,为什么不是两级

1. Spring使用三级缓存来解决循环依赖问题 Spring使用三级缓存来解决循环依赖问题&#xff0c;‌而不是使用两级缓存。‌ 在Spring框架中&#xff0c;‌解决循环依赖的关键在于正确地管理Bean的生命周期和依赖关系。‌循环依赖指的是两个或多个Bean相互依赖&#xff0c;‌如果…

LTD官微(网站)云与枢纽云的差别有哪些,我该如何选择?

数字营销的时代&#xff0c;企业需要依赖互联网工具来实现业务增长和客户转化。LTD官微(网站)云和枢纽云正是应对这种需求的两种不同的解决方案&#xff0c;它们在功能和应用场景上有哪些区别呢&#xff1f;我又该如何选择&#xff1f; 官微云&#xff1a;基础内容管理与生意表…

19-1 LLM之野望 1 – 微软打开1-bit LLM时代

让我们面对现实吧&#xff0c;数字不会说谎。 尽管市场因人工智能而上涨&#xff0c;但其效应显然尚未转化为价值&#xff0c;因为只有不到4&#xff05;的公司使用人工智能来生产商品和服务。 更糟糕的是&#xff0c;虽然一些大公司确实在拥抱人工智能&#xff0c;但高不可攀…

Java案例斗地主游戏

目录 一案例要求&#xff1a; 二具体代码&#xff1a; 一案例要求&#xff1a; &#xff08;由于暂时没有学到通信知识&#xff0c;所以只会发牌&#xff0c;不会设计打牌游戏&#xff09; 二具体代码&#xff1a; Ⅰ&#xff1a;主函数 package three;public class test {…

HarmonyOS ArkUi @CustomDialog 和promptAction.openCustomDialog踩坑以及如何选择

CustomDialog 内使用Link&#xff0c;如何正常使用 错误使用方式&#xff1a; 定义一个函数&#xff0c;在函数内使用弹窗&#xff0c;如下面代码showDialog&#xff1a; 这种使用方式&#xff0c;无法在自定义的CustomDialog内使用 Link&#xff0c;进行父子双向绑定&#x…

查看仓库文件的改变(git-status , git-diff)

当你在进行项目开发的时候&#xff0c;想看一下自己改了什么&#xff0c;使用cmd进入项目的路径 输入命令回车&#xff0c;前面带有modified&#xff0c;说明后面这个文件被修改了前面带有deleted&#xff0c; 说明这个文件被删除了这是Untracked files&#xff0c; 这部分文…