《Linux从练气到飞升》No.16 Linux 进程地址空间

🕺作者: 主页

我的专栏
C语言从0到1
探秘C++
数据结构从0到1
探秘Linux
菜鸟刷题集

😘欢迎关注:👍点赞🙌收藏✍️留言

🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!

文章目录

    • 前言
    • 程序地址空间回顾
      • 示例一
      • 示例二
        • 原因:
    • 什么是地址空间?
      • 小故事
      • 历史上的地址 VS 现在的地址
      • 虚拟地址
        • 1.什么是虚拟地址?
        • 2. 什么是页表?
        • 3. 遗留问题
        • 4. 当我们的程序编译的时候,形成可执行程序的时候,还没有被加载到内存中的时候,请问我们程序内部有地址吗?
        • 5. 程序从编译到执行的过程中,步骤是什么样的?
      • 地址空间的概念
    • 为什么要有地址空间?
      • 1. 隔离和保护
      • 2. 资源管理
      • 3. 内存分布有序化
    • 重新理解什么是挂起?

前言

当你在电脑上运行一个程序时,你可能想知道它是如何在内存中存储和管理数据的。有没有一种方法可以使不同的程序在内存中有自己的专属空间,相互之间不会相互干扰呢?

今天,我们将探索一个令人着迷的概念——进程地址空间。进程地址空间是计算机系统中一项至关重要的技术,它为每个正在运行的程序提供了独立的内存空间,用于存储代码、数据和堆栈等信息。

想象一下,当你同时打开多个应用程序,如浏览器、音乐播放器和游戏时,它们能够在内存中各自存在而不相互干扰。这得益于进程地址空间的隔离和管理。通过进程地址空间,不同的程序拥有自己的内存区域,彼此之间不会相互干扰,从而确保了计算环境的稳定性和安全性。

了解进程地址空间的概念将帮助你更好地理解程序的执行过程、内存管理以及如何避免程序之间的相互影响。它在操作系统、编程和软件开发中都有着重要的应用。通过本次学习,我们将揭开进程地址空间的神秘面纱,探索其在计算机系统中的重要性和实际应用。

现在,让我们一起深入了解进程地址空间,探索程序运行背后的奥秘吧!

研究背景:5.14.0-344.el9.x86_64

程序地址空间回顾

示例一

在我们之前学习c语言的时候,老师可能给大家讲过这样的空间布局图。
在这里插入图片描述
我们可以看到。命令行参数环境变量高于栈的地址、高于堆的地址、高于未初始化数据的地址、高于初始化数据的地址、高于正文代码的地址。事实是否真的如此呢?

我们来验证一下。

测试代码如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int g_unval;
int g_val = 100;


int main(int argc, char *argv[], char *env[])
{
   // int a = 10;
    //字面常量
   const char *str = "helloworld";
   // 10;
   // 'a';
    printf("code addr: %p\n", main);
    printf("init global addr: %p\n", &g_val);
    printf("uninit global addr: %p\n", &g_unval);

    static int test = 10;//static的本质是将该变量开辟在全局区域。
    char *heap_mem = (char*)malloc(10);
    char *heap_mem1 = (char*)malloc(10);
    char *heap_mem2 = (char*)malloc(10);
    char *heap_mem3 = (char*)malloc(10);
    printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
    printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
    printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
    printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)

    printf("test stack addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
    printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
    printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
    printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
    printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)

    printf("read only string addr: %p\n", str);
    for(int i = 0 ;i < argc; i++)
    {
        printf("argv[%d]: %p\n", i, argv[i]);
    }
    for(int i = 0; env[i]; i++)
    {
        printf("env[%d]: %p\n", i, env[i]);
    }

    return 0;
}

结果如下:
在这里插入图片描述

我们还能看到栈向下增长,所以后面的变量地址就越小,而堆向上增长,后面的变量地址就越大。堆和栈是相对而生的,因为他们之间有相当大的一块空间是共享的。

但是我们知道free是在堆上申请空间的,free时只传入堆起始地址,怎么知道要删除几个字节呢?其实在malloc时会把它的属性数据存起来,包括大小、地址等。

我们还需要了解到。用户空间是0~3GB,内核空间是3GB到4GB。

============================================================================

示例二

这里还要讲述一个例子,一开始有一个进程,后来 fork 成了两个,我们在子进程中修改全局变量的值。我们会发现最后子进程的值和父进程的值不相同,但是地址相同,为什么?怎么可能同一个地址同时读取的时候出现了不同的值呢

测试代码如下:

#include <stdio.h>
#include <unistd.h>

int g_val = 100;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 0;
        //child
        while(1)
        {

            printf("I am child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",\
                    getpid(), getppid(), g_val, &g_val);
            sleep(1);
            cnt++;
            if(cnt == 5)
            {
                g_val = 200;
                printf("child chage g_val 100 -> 200 success\n");
            }
        }
    }
    else 
    {
        //father
        while(1)
        {
            printf("I am father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",\
                    getpid(), getppid(), g_val, &g_val);
            sleep(1);
        }
        
    }
}

测试结果如下:
在这里插入图片描述

我们发现,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
  • 地址值是一样的,说明该地址绝对不是物理地址
  • 在Linux地址下,这种地址叫做 虚拟地址
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理

原因:

这里原因也和虚拟地址有关。当我们子进程没有改变父进程的那个全局变量的时候,他们在同一块内存中,但是如果子进程改变了全局变量的值,就会发生写时拷贝,他就到了另一块物理内存上了,但是这两块物理内存所对应的虚拟地址是同一个,也就造成了这样的现象。

在Linux和windows上面验证上面的代码,可能会跑出不一样的结果,我们上面的结论默认只在Linux上有效。

什么是地址空间?

小故事

我们通过一个故事来学习它。

从前有一个大富翁,他有三个孩子。这个大富翁非常富有,拥有很多钱和财产。为了激励他的孩子们,他给每个孩子画了一个大饼。这个大饼代表了10亿美元的财富。每个孩子都认为自己拥有这10亿美元。

大富翁告诉大女儿,她将来会继承家族的产业,成为家族的掌门人。大女儿非常高兴,她开始努力学习,为将来的责任做准备。

大富翁告诉二女儿,她将来会接管他的生意,成为一位成功的商人。二女儿也非常兴奋,她开始学习商业知识,努力提升自己的能力。

大富翁告诉小儿子,他将来会成为街上最亮眼的人。小儿子非常开心,他开始锻炼自己的才艺,努力成为一个出色的表演者。

每个孩子都根据自己拥有10亿美元的想法,制定了自己的计划。大女儿想要投资房地产,二女儿想要扩大生意,小儿子想要在表演领域取得成功。

然而,事实上,这10亿美元只是大富翁给孩子们画的一个饼,他并没有真正给他们这笔钱。但是,每当孩子们向大富翁要钱时,大富翁通常会给他们一些零花钱。因此,每一个人都认为自己是富翁的合法继承人。但是站在上帝视角,我知道他们彼此的存在。我也知道,他们并不是唯一的合法继承人。但是这个老爹给儿子画的饼,我们就叫做地址空间。那么我们现实之中的饼相当于谁呢?他是看得见摸得着的,它叫做物理内存。

我们前面讲述操作系统的时候,讲过先描述再组织,那这里的地址空间就是我们所说的描述,但是他在将来也一定是一种数据结构,要和一个特定的进程关联起来。

历史上的地址 VS 现在的地址

其实在一开始的时候,并没有地址空间这个概念,地址访问的直接就是物理内存,这样的话内存本身是随时可以被读写的,他特别不安全,假如有野指针等问题就很麻烦。

但是现代计算机提出了下面这种方式:
在这里插入图片描述

他引入了虚拟地址的概念。将虚拟地址与物理地址映射起来,要访问物理内存就需要先映射,有人可能会说,最终还是会访问物理内存的,万一我的虚拟地址是一个非法地址呢?

不要急,这时候操作系统会禁止映射就像小时候。你的父母可能会让你把你的压岁钱上交,说存在他那里,怕你弄丢了,等你什么时候要用的时候再给你,但是当你想买玩具的时候,想买很多玩具的时候,你妈觉得那不是必要的,就不会同意,不管你怎么撒泼打滚,是一个道理。

虚拟地址

1.什么是虚拟地址?

其实如硬盘,网卡等外设也有寄存器,我们想把内存的存储空间和外设的寄存器统一编制成当做内存看,但每一个硬件的本身是不同的,所以就需要引入一个虚拟地址空间的东西,它将不同的硬件对应的设备进行编制,所以实际上我们访问的某些硬件是它和虚拟地址就是这样一个关系。

几乎所有的语言,如果他有地址的概念,这个地址一定不是物理地址,而是虚拟地址,一般我们碰不到物理地址,操作系统设计者怕我们误操作导致系统崩溃。

在这里插入图片描述

我们看上面的地址空间。它划分有栈区、共享区、堆区、被初始化数据区等区域。但是我们不用看也知道,为了有效管理内存资源、提供程序运行所需的运行环境,并确保不同程序之间的互相隔离和安全性,他肯定会划分成各个区域的。

事实也的确如此,我们看源码:
在这里插入图片描述

我们会发现它是有通过start或者end的标记值来划分区域。所谓的区域划分本质就是在一个范围里定义startend

所以到现在我们应该知道地址空间,它一定是一种数据结构,而且它里面要有各个区域的划分。我们要知道,每个进程都有他自己的地址空间。该地址空间包含了各种内存区域,如代码段、数据段、堆、栈等。这些区域在进程运行期间可以动态地进行分配和释放。

需要注意的是,不同进程的地址空间可以具有相同的布局,即相同的内存区域类型和相似的地址段,但它们在逻辑上是独立的,相互之间不会干扰或共享内部数据。每个进程都有自己的独立地址空间,使得进程可以在同一台计算机上独立地运行并与其他进程隔离开来。

2. 什么是页表?

页表是处理地址空间和物理内存对应的一种数据结构,是地址空间对象的重要组成部分,用于将进程的虚拟地址映射到物理内存。页表中的每个表项记录了虚拟页和物理页之间的映射关系。当进程访问虚拟地址时,操作系统会根据页表的映射关系将虚拟地址转化为物理地址,从而访问对应的物理内存。

3. 遗留问题

在前面的文章中,我们讲述到fork之后return会被执行两次,它的本质就是对ID进行写入,此时发生了写时拷贝,所以父子进程各自其实在物理内存中有属于自己的变量空间,只不过在我们看到的它是用同一个变量,也就是说,同一个虚拟地址来标识了。
在这里插入图片描述

在fork函数中,父进程和子进程都会执行fork函数内部的return语句。这是因为return语句是一条语句,它在执行时会被两个指令分别执行。所以,当fork函数内部的return语句被执行时,它会被父进程和子进程各自执行一次。

这样的执行机制导致了fork函数的返回值有两个,分别是父进程的返回值和子进程的返回值。父进程的返回值是子进程的进程ID,而子进程的返回值是0。

需要注意的是,当执行return语句时,实际上是对ID进行写入操作。尽管这个ID只有一个是属于父进程的,但是当fork成功之后,当执行return语句时,会发生写入操作,写入之后,父进程和子进程都会执行if和else判断。父进程在判断时使用的是自己的ID,而子进程在判断时使用的是自己的ID。这是因为在fork函数内部的return语句执行时,对ID的写入操作发生了写时拷贝,所以父进程和子进程各自拥有自己的ID。

4. 当我们的程序编译的时候,形成可执行程序的时候,还没有被加载到内存中的时候,请问我们程序内部有地址吗?

答案是已经有地址了。地址空间不仅仅理解成为是操作系统要遵守的,其实编译器也要遵守,在编译器编译代码的时候,就已经给我们形成了各个区域代码区,数据区的,并且采用了和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址,故,程序在编译的时候,每一个字段早已经具有了一个虚拟地址。

在编译器的视角里,在一个程序内部的各种地址关系,其实这个地址不叫做虚拟地址,它叫做逻辑地址。只不过在 Linux 下,逻辑地址、线性地址和物理地址其实是一模一样的,因为我们的 Linux 认为所有的那么起始地址全部都是从 0 开始的。

可执行程序在编译阶段就已经具有地址。在编写C或C++代码时,编译器会为每个函数和变量分配对应的地址。这些地址是相对于程序内部的,可以理解为虚拟地址。编译器会根据程序的结构和代码逻辑,为每个函数和变量分配合适的地址。

当程序被加载到内存中时,操作系统会为其分配一块地址空间,这个地址空间是操作系统给进程分配的,也被称为虚拟地址空间。在这个虚拟地址空间中,程序的代码、数据和其他区域会被映射到对应的物理地址上。

因此,可以说在程序编译阶段,程序内部已经具有地址,这些地址是相对于程序内部的虚拟地址。当程序加载到内存后,操作系统会将虚拟地址映射到物理地址上,使得程序可以在内存中正确执行。

5. 程序从编译到执行的过程中,步骤是什么样的?

a. 编译,将每个函数和变量分配对应的地址,这个地址是虚拟地址
b. 加载,程序被加载到内存中,操作系统会为其分配一块地址空间,这个地址空间是操作系统给进程分配的,也被称为虚拟地址空间。程序会把自己编译时分配的虚拟地址与地址空间的虚拟地址相对应,在这个虚拟地址空间中,程序的代码、数据和其他区域会被映射到对应的物理地址上。
c. 运行,task_struct会被加载到CPU中,CPU在运行过程中需要调用哪一个函数,就会通过虚拟地址找到对应的物理地址,然后调用该函数,调用该函数后也会通过页表、地址空间、PCB来返回对应的地址,往复循环。

地址空间的概念

地址空间是操作系统为进程专门设计的一种内核数据结构。它是进程在运行时所使用的虚拟内存空间的抽象表示。在计算机系统中,每个进程都有自己独立的地址空间,用于存储程序的指令、数据和堆栈等信息。

地址空间的定义包括了进程的线性区域的划分每个区域由起始地址和结束地址来标定。这些区域可以包括代码段、数据段、堆和栈等。每个区域内的地址可以不连续,但在指定的范围内都可以被访问。
在这里插入图片描述
地址空间的设计目的是为了提供一种抽象的方式来管理进程的内存使用。它使得每个进程都可以拥有自己独立的地址空间,而不会相互干扰。通过地址空间的映射机制,操作系统可以将进程的虚拟地址转化为物理内存地址,从而避免了进程对内存的直接访问
在这里插入图片描述
总之,地址空间是操作系统为进程提供的一种抽象概念,用于管理进程的内存使用。它定义了进程的虚拟内存空间的划分和映射机制,确保每个进程都可以独立地访问自己的内存空间,保证了进程之间的隔离和安全性。

为什么要有地址空间?

1. 隔离和保护

  • 凡是非法的访问或者映射操作系统都会识别到并终止你这个进程,
  • 可以有效的保护物理内存,因为地址空间和页表是操作系统创建并维护的,
  • 也就意味着凡是想使用地址空间和页表进行映射,也一定要在操作系统的监管之下来进行访问,
  • 他还保护了物理内存中所有的合法数据,包括各个进程以及内核的相关有效数据。

2. 资源管理

因为有地址空间的存在,也因为有页表的映射的存在,我们的物理内存中是不是可以对未来的数据进行任意位置的加载?

当然可以,物理内存的分配可以做到和进程的管理没有关系。这样就实现了内存管理模块和进程管理模块的解耦合。

所以我们在C、C++语言上new和malloc空间的时候,本质是在虚拟地址空间上申请的。试想一下,如果我申请了物理空间,但是我不立马使用,是不是就会造成了空间的浪费了?当然会。 而本质上因为有地址空间的存在,上层申请空间其实是在地址空间上申请的,物理内存可以甚至一个字节都不给你,而当你真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存构建列表映射关系。然后再让你进行内存的访问。而对物理地址空间访问是由操作系统自动完成的,用户包括进程完全灵感知。这样也就是延迟分配的策略,它可以提高整机的效率,它使内存的有效使用几乎为100%。有了地址空间的存在,我们可以以有序化的视角去看待进程的代码和数据在物理内存中的分布情况。这样可以更好地确定代码和数据的起始地址,并且便于操作系统进行统一管理。同时,有序化的内存分布也使得我们能够更好地检测越界访问等问题。

3. 内存分布有序化

因为在物理内存中理论上可以任意位置的加载,那是不是物理内存中的几乎所有的数据和代码在内存中是乱序的呢?

确实如此,因为有页表的存在,它可以将地址空间上的虚拟地址和物理地址进行映射。

他就可以做到说,所有的内存分布有序化。我们知道地址空间是操作系统给进程画的大饼。

进程的独立性可以通过地址空间加页表的方式实现,结合上一条来说,进程要访问的物理内存中的数据和代码,可能目前并没有在物理内存中存在,也可以让不同的进程映射到不同的物理内存,这样就很容易实现了进程的独立性。

因为有地址空间的存在,每一个进程都认为自己拥有4GB空间,并且各个区域是有序的。进而可以通过列表映射到不同的区域来实现进程的独立性,每一个进程不知道,也不需要知道其他进程的存在。

重新理解什么是挂起?

加载的本质就是创建进程,那么是不是非得立马把所有的程序的代码和数据加载到内存中,并创建内核数据结构,建立映射关系?

不是这样的,在最极端情况下,只有内核结构被创建出来了,是什么情况的,此时,他并没有被调用的时候,就是这种情况,这个状态也就是新建状态,理论上我们是可以实现对程序的分批加载的,既然可以分批加载,当然也可以分批换出。进程的数据和代码被换出了,就叫做挂起了。

页表映射的时候不仅仅映射的是内存,磁盘中的位置也可以映射。

比如说加载一些大型的游戏,如果一次性全部加载,物理内存肯定是不够的,然而我们分批加载,加载了一部分,又换出一部分,如此往复便实现了游戏的加载。还有就是当我们加载游戏时。某个进程可能需要等待其他资源,甚至这个进程短时间不会再被执行了,他此时的状态就是阻塞了,他此时可能就会把物理内存唤出到磁盘中,等待下一次的唤入。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

matlab使用教程(20)—插值基础

1.网格和散点样本数据 插值是在位于一组样本数据点域中的查询位置进行函数值估算的方法。函数值是根据最接近查询点的样本数据点计算的。MATLAB 根据样本数据的结构&#xff0c;可以执行两种插值。样本数据可以形成网格&#xff0c;也可以是分散的。 网格化的样本数据使得插值…

Arduino之esp8266

今天&#xff0c;捣鼓了Arduino和esp8266,发现有两款比较好的软件&#xff08;Arduino IDE以及Mixly软件&#xff09;可以将程序下载至esp8266中&#xff0c;而且两者的编程语言都是一样的&#xff0c;都是基于Arduino编程语言&#xff0c;只不过一个Mixly更注重图形编程&#…

cuOSD(CUDA On-Screen Display Library)库的学习

目录 前言1. cuOSD1.1 Description1.2 Getting started1.3 For Python Interface1.4 Demo1.5 Performance Table 2. cuOSD案例2.1 环境配置2.2 simple案例2.3 segment案例2.4 segment2案例2.5 polyline案例2.6 comp案例2.7 perf案例 3. cuOSD浅析3.1 simple_draw函数 4. 补充知…

大数据平台需要做等保测评吗?怎么做?

大数据时代的数据获取方式、存储规模、访问特点、关注重点都有了很大不同&#xff0c;所以保证大数据平台数据安全尤其重要。因此不少人在问&#xff0c;大数据平台需要做等保测评吗&#xff1f;怎么做&#xff1f; 大数据平台需要做等保测评吗&#xff1f; 大数据平台是需要做…

创建和运行 Ansible 临时命令

创建和运行 Ansible 临时命令 作为系统管理员&#xff0c;您需要在受管节点上安装软件。 请按照正文所述&#xff0c;创建一个名为 /home/curtis/ansible/adhoc.sh 的 shell 脚本&#xff0c;该脚本将使用 Ansible 临时命令在各个受管节点上安装 yum 存储库&#xff1a; 存储库…

EasyExcel 导出报空指针NullPointerException

java.lang.NullPointerException: null at sun.awt.FontConfiguration.getVersion(FontConfiguration.java:1264) at sun.awt.FontConfiguration.readFontConfigFile(FontConfiguration.java:219) 这是jdk缺少字体库问题 在官网也给出解决答案&#xff1a; 1.安装少了字体库…

jvm-虚拟机栈

1.栈的存储单位 栈是运行时单位&#xff0c;而堆是存储的单位 栈解决程序的运行问题&#xff0c;即程序如何执行&#xff0c;或者说如何处理数据。堆解决的是数据存储问题&#xff0c;即数据怎么放&#xff0c;放在哪儿 java虚拟机栈 早期也叫java栈&#xff0c;每个线程在创建…

lesson9: C++多线程

1.线程库 1.1 thread类的简单介绍 C11 中引入了对 线程的支持 了&#xff0c;使得 C 在 并行编程时 不需要依赖第三方库 而且在原子操作中还引入了 原子类 的概念。要使用标准库中的线程&#xff0c;必须包含 < thread > 头文件 函数名 功能 thread() 构造一个线程对象…

Java之抽象类

Java之抽象类 抽象类概念抽象类如何使用抽象类的特性 作者简介&#xff1a; zoro-1&#xff0c;目前大一&#xff0c;正在学习Java&#xff0c;数据结构等 作者主页&#xff1a;zoro-1的主页 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f49…

Vue2-全局事件总线、消息的订阅与发布、TodoList的编辑功能、$nextTick、动画与过渡

&#x1f954;&#xff1a;高度自律即自由 更多Vue知识请点击——Vue.js VUE2-Day9 全局事件总线1、安装全局事件总线2、使用事件总线&#xff08;1&#xff09;接收数据&#xff08;2&#xff09;提供数据&#xff08;3&#xff09;组件销毁前最好解绑 3、TodoList中的孙传父&…

乖宝宠物上市,能否打破外资承包中国宠物口粮的现实

近日&#xff0c;乖宝宠物上市了&#xff0c;这是中国宠物行业成功挂牌的第三家公司。同时&#xff0c;昨日&#xff0c;宠物行业最大的盛事“亚洲宠物展”时隔3年&#xff0c;于昨日在上海成功回归。 这两件事情的叠加可谓是双喜临门&#xff0c;行业能够走到今天实属不易&…

java网络编程

目录 1. 什么是网络编程? 2. 网络编程三要素 2.1 IP 2.1.1 常见CMD命令 2.1.2 InetAddress 2.2 端口号 2.3 协议 3. UDP通信程序 3.1 UDP的三种通信方式 4. TCP通信程序 4.1 三次握手四次挥手 1. 什么是网络编程? 在网络通信协议下&#xff0c;不同计算机上运行的程…

生成式AI和大语言模型 Generative AI LLMs

在“使用大型语言模型(LLMs)的生成性AI”中&#xff0c;您将学习生成性AI的基本工作原理&#xff0c;以及如何在实际应用中部署它。 通过参加这门课程&#xff0c;您将学会&#xff1a; 深入了解生成性AI&#xff0c;描述基于LLM的典型生成性AI生命周期中的关键步骤&#xff…

基于Java的ssm菜匣子优选系统源码和论文

基于Java的ssm菜匣子优选系统039 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&…

unity发布WebGL遇到的坑(持续更新)

1、unity默认字体在网页中不会显示 解决方法&#xff1a;自己新导入一个字体&#xff0c;使用导入的字体 2、之前打过包并运行过&#xff0c;后面又在unity中进行了修改&#xff0c;重新打包&#xff0c;运行发现还是修改之前的效果&#xff0c;虽然是新包&#xff0c; 解决方…

Linux下gdb调试

1.基本命令操作 2.调试方式启动运行无参程序 以下是linux下GDB调试的一个实例&#xff0c;先给出一个示例用的小程序&#xff0c;C语言代码&#xff1a; main.c #include <stdio.h>void Print(int i){printf("hello,程序猿编码 %d\n", i); }int main(int argc…

Python爬虫解析工具之xpath使用详解

文章目录 一、数据解析方式二、xpath介绍三、环境安装1. 插件安装2. 依赖库安装 四、xpath语法五、xpath语法在Python代码中的使用 一、数据解析方式 爬虫抓取到整个页面数据之后&#xff0c;我们需要从中提取出有价值的数据&#xff0c;无用的过滤掉。这个过程称为数据解析&a…

【实战】十一、看板页面及任务组页面开发(三) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(二十五)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求五、CSS 其实很简单 - 用 CSS-in-JS 添加样式六、用户体验优化 - 加载中和错误状态处理七、Hook&…

GB28181国标平台测试软件NTV-GBC(包含服务器和模拟客户端)

GB28181国标平台测试软件NTV-GBC用于对GB28181国标平台进行测试(测试用例需要服务器软件&#xff0c;服务器软件可以是任何标准的国标平台&#xff0c;我们测试使用的是NTV-GBS&#xff09;&#xff0c;软件实现了设备注册、注销、目录查询&#xff0c;消息订阅、INVITE&#x…

剑指offer(C++)-JZ64:求1+2+3+...+n(算法-位运算)

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 题目描述&#xff1a; 求123...n&#xff0c;要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句&…
最新文章