Linux系统编程——进程控制

目录

一,进程创建

1.1 fork回顾

1.2 写时拷贝

1.3 fork用处

1.4 fork调用失败原因

二,进程退出

2.1 进程退出场景

2.2 mainCRTStartup调用

2.3 进程退出码

2.3.1 main函数返回值

2.3.2 strerror 

​编辑 2.3.3 命令的退出码

2.4 进程正常退出

2.5 进程异常退出

三,进程等待

3.1 为什么要有进程等待?

3.2 wait函数和waitpid函数的使用

 3.3 wait函数和waitpid的status参数

3.4 多进程阻塞等待

3.5 非阻塞等待

四,进程程序替换

4.1 什么是进程程序替换?为什么要有这个?

4.2 execl函数

4.3 其它exec函数

​编辑 

4.4 execve

五:一些问题解答

5.1 wait和waitpid如何拿到进程的退出信息的呢?为什么不直接用全局变量拿到退出码?

5.2 僵尸进程中曾经new和malloc的空间会释放码?


一,进程创建

1.1 fork回顾

fork的详细介绍请见上篇博客第四节——这里只做简单回顾Linux系统编程 —— 进程概念,环境变量,虚拟地址空间总结(收藏向)-CSDN博客

fork()函数可以创建子进程,由于fork也是个函数,所以也有返回值,会对父进程返回子进程的pid,对子进程返回0。具体用法如下:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    printf("我是主进程");
    pid_t id =fork();
    if(id<0)
    {
        printf("创建子进程失败\n");
        return 1;
    }
    else if(id==0)
    {
        printf("我是子进程:pid:%d,ppid:%d\n",getpid(),getppid());
    }
    else 
    {
        printf("我是父进程:pid:%d,ppid:%d\n",getpid(),getppid());      
    }
    return 0;
}

问题:fork创建子进程时,OS做了什么?

解答:创建子进程,系统里就多了一个进程,所以先创建进程的PCB,地址空间和页表,将程序在磁盘上的地址加载到内存中,然后把程序的代码和数据从磁盘加载到物理内存中,然后把PCB放到CPU的运行队列里等待被调度,当该进程开始调度时,就通过虚拟地址的映射找到在物理内存的相关代码,然后就按照顺序语句从上往下在进程内部执行代码

进程 = 内核数据结构 + 进程代码和数据 (前者OS维护,后者一般从磁盘上来,就是C/C++程序加载之后的结果)

1.2 写时拷贝

进程具有独立性,所以创建子进程时分配给它的内核结构是它独有的,理论上进程也要有自己的代码和数据。可是一般而言,子进程只是随着父进程的加载而创建出来的,也就是说子进程没有属于自己的代码和数据,它只能用父进程的代码和数据,但这又和进程的独立性相违背,为啥?

①代码:对进程来说,代码都是已经加载完成的,只能读取不能写入,所以代码数据父子共享没问题

②数据:由于数据可能会被修改,所以必须分离,但是不能直接拷贝分离,因为可能会拷贝子进程根本用不到的数据空间,而且子进程也可能只读数据不做修改,这种情况再进行拷贝就会造成时间和空间的浪费。所以采用写时拷贝的技术,写时拷贝请参照上篇博客第十节:

Linux系统编程 —— 进程概念,环境变量,虚拟地址空间总结(收藏向)-CSDN博客

 如下代码:

#include<stdio.h>
int main()
{
  const char *str1 = "aaa";
  const char *str2 = "aaa";
  printf("%p\n%p\n",str1,str2); 
  return 0;
}

 

 第一个指针创建常量字符,第二个指针也指向这个常量字符时,就不再额外开空间了,所以连编译器编译时都知道节省空间来优化,OS没有理由不优化,所以创建子进程的时候OS采用了写时拷贝的技术,就是字面意思:“你要数据的候再给你拷贝”。

写时拷贝使得父子进程得以彻底分离,保证了进程的独立性,同时也是高效使用内存的一种表现

1.3 fork用处

①一个进程希望复制自己,使执行父进程代码的同时子进程执行不同的代码,例如父进程等待客户端请求数据,子进程处理请求数据

②一个进程要执行一个不同程序,例如子进程创建后来后,调用exec函数,这个后面的进程程序替换再讲

1.4 fork调用失败原因

系统进程过多,或者是用户的进程超过了限制

如下代码,由于下列代码执行后进程满了fork报错,系统可能会出问题,为了各位主机的安全考虑 就不贴代码啦

可以看见创建将近800个进程后再创建就失败了 

二,进程退出

2.1 进程退出场景

进程退出只能有三种情况:

①代码运行完成,结果正确

②代码运行完成,结果不对

③代码未运行完成,异常退出(OS通过发送信号的方式强制终止进程)

2.2 mainCRTStartup调用

为什么main函数也有返回值呢?有人会说main函数也是函数所以有返回值,那么既然main是函数,那么是谁调用的main函数呢?下面是VS2022编译器的main函数层层调用的示例图:(下面代码需要在release调试环境下用逐语句依次执行才可显示)

所以其实main函数最开始是被一个叫做mainCRTStartup的函数调用的,而这个函数又是通过加载器被操作系统所调用的,也就是说main函数是间接被操作系统调用的。

main函数只是用户级别的代码的入口,而程序的入口还是操作系统 

2.3 进程退出码

2.3.1 main函数返回值

通过2.2,我们知道main函数既然是被操作系统调用的,那么main函数作为被调用者就应该给调用者返回退出信息,而这个退出信息就是以退出码的形式作为main函数的返回值返回,一般以返回0代表代码执行完毕,非0代表代码执行过程中出现错误,但是我们前面写的程序都是小程序,代码数不多,所以一般以0返回。

main函数的返回值就是进程的退出码,我们可以用" echo $? "来查看最后一次进程退出时的退出码

问题:为什么0代表执行成功,非0代表错误?

解答: 代码执行完毕只有两种情况,成功和错误,成功就是我们想要的只有一种情况,但是错误往往是有很多的,这个大家都懂,所以我们就用这些非0的数字代表代码执行错误的原因

2.3.2 strerror 

 但是退出码是方便计算机返回设定的,而我们并不清楚像“ 1234 ”这样的退出码到底是什么意思,所以我们需要一个能将错误码转化成字符串显示给用户的方案 —— ”strerror“,打印退出码代表的字符信息,如下代码:

#include<stdio.h>
#include<string.h>
int main()
{
  int i = 0;
  for(i = 0; i < 10; i++)
  {
      printf("[%d]: %s\n", i, strerror(i));
  }
  return 0;
}

 2.3.3 命令的退出码

Linux中的各种命令也是C语言写的程序,所以也会有退出码,我们使用命令后照样也可以通过“ echo $? ”查看退出码

2.4 进程正常退出

return

main函数使用的return退出就是我们最常用的方法来退出进程

exit

exit函数也是常用的退出进程的方法,但是exit在进程退出前多做了一系列工作:

1,执行用户定义的atexit或on_exit定义的清理函数

2,关闭所有打开的流,所有缓存数据均被写入

3,调用_exit系统调用终止进程

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

void Hello()
{
  printf("hello world"); //未加\n,不会刷新缓冲区
  exit(1);
}

int main()
{
  Hello();
  return 0;
}

 

_exit

_exit是系统调用的进程退出函数,但是我们一般不用_exit来退出进程,因为_exit推出前不会有exit中的推出前工作,它只是退出,比如上面的代码如果把exit换成_exit就不会刷新缓冲区了,hello world不会打印

而这三个函数的关系呢,就是下面这个图了:

_exit就是进程直接退出,什么也不干,exit就是在推出前做了些善后工作再退出,然后return,在C语言语法实现中就是调用的exit() 

2.5 进程异常退出

前面说过:所有的进程异常终止都是操作系统干的,具体有下面两种方式:

①操作系统向进程发送特定信号导致进程异常退出,例如kill -9和Ctrl+C

②代码错误导致进程运行时异常退出,例如野指针,越界访问和除0等(其实代码的逻辑错误异常退出本质也是操作系统检测到了代码的非法操作然后向进程发送信号的方式终止进程的)

三,进程等待

3.1 为什么要有进程等待?

①子进程退出父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,造成内存泄漏

②子进程被创建出来是要它去做事的,它的事做得咋样父进程需要关心,也就是进程退出的三个结果

③然后就是子进程把结果返回父进程了,那么父进程咋收到子进程的退出信息呢?那么包括回收子进程申请的相关内存资源,还有父进程获得子进程退出结果都需要进程等待来完成

3.2 wait函数和waitpid函数的使用

如下图,是man文档的2号文档对wait接口的说明:

wait函数

如下代码:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        exit(1);//标识进程运行完毕结果不对
    }
    else if(id==0)
    {
        //子进程
        int cnt =3;
        while(cnt)
        {
            printf("cnt:%d,我是子进程,pid:%d,ppid:%d\n",cnt,getpid(),getppid());
            sleep(1);
            cnt--;
        }
        exit(0);
    }
    else
    {
        //父进程
        printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid());
	    sleep(6); //子进程跑3秒,父进程等6秒,所以可以看到子进程有3秒的Z状态,然后wait后Z状态就没了
        pid_t ret = wait(NULL); //阻塞式等待,简单来说就是卡在这,等子进程完成
        //pid_t ret = waitpid(id, NULL, 0); //waitpid的效果和wait一样,wiatpid的第一个参数表示要等待的进程的pid,第三个参数为0表示阻塞等待
        if(ret > 0)
        {
          printf("等待子进程成功,ret:%d\n", ret);
        }
    }
}

可以看到完美地消除了Z进程,有效解决了僵尸进程问题 

 3.3 wait函数和waitpid的status参数

那么只解决了僵尸进程问题还不够,因为父进程还需要知道子进程的退出码滴,当然也是通过wait和waitpid函数来获取,这时候就要着重讲讲它们的参数了

该参数是一个输出型参数,是一个指针,为NULL时不会获取到子进程退出码,不为NULL时会传入子进程的退出码。参数这里是一个指针,那么指针指向的是一个16比特位的整型。但是status不能当作一个简单的整型来对待,一共16个比特位,而操作系统对这16个比特位做了划分,如下图:

 在status一个int的16个比特位中高8位代表进程的退出状态也就是退出码,后7位表示终止信号

进程最终的结果有两种,一种是正常退出,一种是被信号所杀,而两种退出方式对status做的修改也不同

先看下正常退出对status做的修改:

 信号所杀做的修改:

 所以获取退出码和退出信号时,我们需要做一些额外的处理,如下:

int status;
wait(&status);
waitpid(pid, &status, 0);

exitCode = (status >> 8) & 0xFF;  //退出码
exitSignal = status & 0x7F;       //退出信号

//当然上面的方式用起来肯定是比较别扭的而且不美观,所以系统中提供了两个宏来方便用户操作
IfexitSignal = WIFEXITED(status); //返回值为bool类型,用于查看进程是否正常退出,检查是否收到终止信号
exitCode = WEXITSTATUS(status);   //获取进程退出码

 注意,凡是在C语言中,像这种全大写的特殊字符,很大可能就是宏定义

3.4 多进程阻塞等待

上面的都是父进程创建一个进程,然后单独等待一个子进程退出的场景,但是我们也可以同时创建多个子进程,然后让父进程依次等待子进程退出,如下代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	pid_t ids[10]; //该数组存储子进程的pid
    int i = 0;
	for (i = 0; i < 10; i++)
    {
	   pid_t id = fork(); //创建10个子进程
	   if (id == 0)
       {
		  printf("child[%d] created success pid:%d, \n",i, getpid());
		  sleep(3);
		  exit(i); //使每个子进程的退出码设置为创建它的编号
	   }
	   ids[i] = id; //这一条是父进程执行
	}
	for (i = 0; i < 10; i++)
    {
	  	int status = 0;
	    pid_t ret = waitpid(ids[i], &status, 0);
	    if (ret >= 0)
        {
		    printf("wiat child success..PID:%d\n", ids[i]);
		    if (WIFEXITED(status))
            {
			   //正常退出
			   printf("exit code: %d\n", WEXITSTATUS(status));
		    }
		    else
            {
			   //信号终止
	  	       printf("signal kill: %d\n", status & 0x7F);
		    }
		}
	}
	return 0;
}

 

3.5 非阻塞等待

前面说到过waitpid的第三个参数option,为0时就是代表阻塞等待,那么不为0的时候呢?

阻塞等待:一般都是在内核中阻塞,等待被唤醒 --> 伴随被唤醒 --> scanf,cin --> 必定封装系统调用

非阻塞等待:我们父进程通过调用waitpid来进行等待,如果子进程没有退出,那么waitpid立即返回,waipid的伪代码实现如下:

waitpid(chile_id, status, flag);
if(status == 进程已经退出) {
    return child_pid;
}
else if(status == 进程还没退出) {
    if(flag == 0) {} //如果falg为0,那么就阻塞
    else if(flag == WNOHANG) return 0; //直接返回,不阻塞进程
    return 0;
}
else {
    //出错了或者其他原因
    return -1;
}

 当waitpid第三个参数为WNOHANG时,就代表父进程非阻塞等待,这个其实也是宏定义,我们可以查看,如下图:

所以我们可以用这个特性, 让子进程忙的时候父进程做点其它的事,但是又能让父进程获取到子进程的退出信息,如下代码:

#include<iostream>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<vector>

typedef void (*handler_t) (); //函数指针类型
std::vector<handler_t> handlers ; //函数指针数组

void fun_one() { printf("这是一个临时任务1\n"); }
void fun_two() { printf("这是一个临时任务2\n"); }
//设置对应的方法回调
//以后想让父进程闲的时候执行其他方法时,只要向Load里面注册,就可以让父进程执行对应的方法
void Load()
{
  handlers.push_back(fun_one);
  handlers.push_back(fun_two);
}

int main()
{
  pid_t id = fork();
  if(id==0)
  {
    //子进程
    int cnt = 5;
    while(cnt)
    {
      printf("我是子进程:%d\n",cnt--);
      sleep(1);
    }
    exit(11);
  }
  else
  {
    int quit=0;
    while(!quit)
    {
      int status =0;
      pid_t res = waitpid(-1,&status,WNOHANG); //非阻塞等待
      if(res>0)
      {
        //等待成功,子进程退出
        printf("等待子进程退出成功,退出码:%d\n",WEXITSTATUS(status));
        quit = 1;
      }
      else if(res==0)
      {
        //等待成功,但子进程未退出
        printf("子进程还在运行还未退出,父进程再等一等或者做其他事情\n");
        if(handlers.empty()) Load();
        for(auto e : handlers)
        {
          //执行处理其他任务
          e();
        }
      }
      else{
        //等待失败
        printf("wait失败");
        quit=1;
      }
      sleep(1);
    }
  }
}

四,进程程序替换

4.1 什么是进程程序替换?为什么要有这个?

fork之后,父子进程各自执行代码的一部分,代码数据只读,父子进程共享;变量数据写时拷贝

那么如果子进程我不执行原来的代码了,我想执行一份全新的程序呢?就是父子代码不共享了,子进程执行自己的代码。

程序替换是通过特定的接口,加载磁盘上一个全新的程序(代码和数据),加载到子进程的地址空间中

问题:进程替换的时候有没有创建新的子进程?

解答:没有创建进程。进程为内核数据结构+代码和数据,把一个全新的程序加载到内存里,仅仅是重新建立了映射关系,进程的内核数据结构,状态和优先级等未发生变化,所以没有创建进程

4.2 execl函数

先看man execl的查询:

可以看到exec系列函数一共有6个,我们先搞第一个execl。 

第一个参数是一个const char指针代表其它可执行程序的绝对路程或相对路径,第二个参数是一个可变参数列表,表示你要替换的可执行程序后面要带的参数,以NULL结尾。比如我要执行ls函数,如下代码:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>

int main(int argc,char* argv[],char *env[])
{
  pid_t id = fork();
  if(id == 0)
  {
    //子进程
    printf("子进程开始运行,pid:%d\n",getpid());
    sleep(3);
    execl("/usr/bin/ls","ls","--color=auto", "-a","-l",NULL);
    exit(1);
  }
  else{
    //父进程
    printf("父进程开始运行,pid:%d\n",getpid());
    int status =0;
    pid_t id =waitpid(-1,&status,0);//阻塞等待
    if(id>0)
    {
      printf("wait success,exit code:%d\n",WEXITSTATUS(status));
    }
  }
}

 

刚开始父子进程都开始运行,但是3秒后才有ls -a -l的内容,虽然不知道父子进程谁先开始的,但是肯定是父进程最后结束

问题:为什么要创建子进程让子进程来执行execl函数?

解答:如果不创建子进程,那么替换的就是父进程,我们需要让父进程聚焦在读取数据解析数据,指派子进程执行代码的功能(类似工头和工人的关系),所以我们要保证子进程在使用execl替换的时候不影响父进程。

(加载程序之前,父子进程代码共享,数据写时拷贝,当子进程替换的时候,其实也是一种写入,这时候代码也要写时拷贝,并且要将父子进程的代码分离) 

4.3 其它exec函数

 

 ①execlp

这个函数就是在execl后面加了个“ p ” ,p通常表示环境变量,这个函数作用和execl一样,只是第一个参数可以不传路径了,我们输入可执行程序名,它会直接在环境变量当中找,比如我要替换ls:

execlp("ls","ls", "--color=auto", "-l", NULL);

②execle

这个函数和execlp相比多了一点东西又少了一点东西。第一个参数和execl仍然是路径,第二个参数为可变模板列表,第三个参数是新增的,该参数可以由用户自己定义,并在替换后被另一个进程使用

char* myenvp = {"MYVAL=2024", NULL};
execle("./mytest", "mytest", NULL, myenvp);

 ③execv

execl最后的l我们可以看成list的意思,那么execv的v我们就可以看成vector。

第一个参数是路径,后面跟的是一个指针数组了,数组里面每一个指针都指向一个字符串,每个字符串都是替换过后的进程的参数,比如我要用execv将当前进程替换成ls,如下代码:

char* myargv[] = {"ls", "--color=auto", "-l"};
execv("/usr/bin/ls", myargv);

④execvp

和execlp一样,第一个参数不再是路径,可以直接传可执行程序名,然后系统回去环境变量当中找,比如我要执行ls,如下代码

char* myargv[] = {"ls", "--color=auto", "-l"};
execv("ls", myargv);

⑤execvpe

算是这几个函数里最难用的一个吧,也是把自定义环境变量传给替换后的进程。如下代码:

char* myargv[] = {"mytest", NULL}
char* myenvp[] = {"MYVAL=2024", NULL};
execvpe("./mytest", myargv, myenvp);

4.4 execve

上面的要想在man文档查,发现只能“man 3 execl”,而不能“man 2 execl”,这说明上面的6个函数都不是系统调用,那么真正实现替换的是哪个系统调用呢?

man 2 execve后可以看到下面内容:

可以发现execve的参数和execvpe一模一样,上面的6个函数都是都是对系统调用的execve做了封装,因为这么做是为了满足上层用户的不同调用场景

五,一些问题解答

5.1 wait和waitpid如何拿到进程的退出信息的呢?为什么不直接用全局变量拿到退出码?

僵尸进程是已经死掉的进程,代码和数据可以释放,但是进程的内核数据结构也就是PCB不能释放,而PCB内部也保留了进程退出时的各种对出结果,所以wait和waitpid本质就是去读取子进程的task_struct里的退出信息来获取子进程的退出信息。

不能用全局变量是因为进程具有独立性,而且对全局变量做修改时会发生写时拷贝

5.2 僵尸进程中曾经new和malloc的空间会释放码?

前面讲过,僵尸进程是已经死掉的进程,代码和数据会被释放,进程new和malloc申请的空间也会随着进程死掉被释放,因为new和malloc是用户申请的空间,僵尸进程的PCB是操作系统开辟的空间,前者属于用户层的,后者是属于内核层的,所以僵尸进程的内存泄漏是属于操作系统内部的内存泄漏,与用户层无关。

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

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

相关文章

LeetCode-258. 各位相加【数学 数论 模拟】

LeetCode-258. 各位相加【数学 数论 模拟】 题目描述&#xff1a;解题思路一&#xff1a;循环解题思路二&#xff1a;进阶 O(1)解题思路三&#xff1a; 题目描述&#xff1a; 给定一个非负整数 num&#xff0c;反复将各个位上的数字相加&#xff0c;直到结果为一位数。返回这个…

记录一次pods 导入 SocketRocket库的经历

折腾一上午&#xff0c;brew 安装成功了 cococapod 然后项目启动下载一个SocketRocket库 下载成功后总是报错&#xff1a; 睡了2个多小时&#xff0c;我在qq就交流群里求助&#xff1a; 终于把项目管理&#xff0c;在命令行里执行这句&#xff1a; open chat_app.xcworkspace…

企业破产重整:从“至暗时刻”到“涅槃重生”

今天我们不谈星辰大海&#xff0c;而是要潜入商业世界的深海区&#xff0c;探索那些濒临绝境的企业是如何借助“破产重整”的神秘力量&#xff0c;实现惊天大逆转的&#xff01; 一、破产重整&#xff0c;到底是个啥&#xff1f; 想象一下&#xff0c;企业像是一位远航的船长…

【Redis】Redis 事务

Redis 的事务的本质是一组命令的批处理。这组命令在执行过程中会被顺序地、一次性 全部执行完毕&#xff0c;只要没有出现语法错误&#xff0c;这组命令在执行期间不会被中断 1.事务特性 仅保证了数据的一致性 这组命令中的某些命令的执行失败不会影响其它命令的执行&#xff…

geoserver SQL注入、Think PHP5 SQL注入、spring命令注入

文章目录 一、geoserver SQL注入CVE-2023-25157二、Think PHP5 SQL注入三、Spring Cloud Function SpEL表达式命令注入&#xff08;CVE-2022-22963&#xff09; 一、geoserver SQL注入CVE-2023-25157 介绍&#xff1a;GeoServer是一个开源的地理信息系统&#xff08;GIS&#…

Java 开发 框架安全:Spring 漏洞序列.(CVE-2022-22965)

什么叫 Spring 框架. Spring 框架是一个用于构建企业级应用程序的开源框架。它提供了一种全面的编程和配置模型&#xff0c;可以简化应用程序的开发过程。Spring 框架的核心特性包括依赖注入&#xff08;Dependency Injection&#xff09;、面向切面编程&#xff08;Aspect-Or…

clang:在 Win10 上编译 MIDI 音乐程序(二)

先从 Microsoft C Build Tools - Visual Studio 下载 1.73GB 安装 "Microsoft C Build Tools“ 访问 Swift.org - Download Swift 找到 Windows 10&#xff1a;x86_64 下载 swift-5.10-RELEASE-windows10.exe 大约490MB 建议安装在 D:\Swift\ &#xff0c;安装后大约占…

VPN方案和特点

VPN方案和特点 VPN&#xff0c;或者称为虚拟专用网络&#xff0c;是一种保护你的在线安全和隐私的技术。它可以创建一个加密的连接&#xff0c;使你的在线活动对其他人不可见。以下是一些常见的VPN协议和它们的特点&#xff1a; 开放VPN (OpenVPN)&#xff1a;这是一种极为可…

【京东电商API接口】 | 京东某商品销量数据分析可视化

Python当打之年 当打之年&#xff0c;专注于各领域Python技术&#xff0c;量的积累&#xff0c;质的飞跃。后台回复&#xff1a;【可视化项目源码】可获取可视化系列文章源码和数据 本期将利用Python分析「京东商品数据接口」&#xff0c;希望对大家有所帮助&#xff0c;如有疑…

BGP第二篇(bgp邻居状态及影响邻居建立的因素)

1、bgp邻居状态 BGP对等体的交互过程中存在6种状态机&#xff1a; 空闲&#xff08;Idle&#xff09; 连接&#xff08;Connect&#xff09; 活跃 &#xff08;Active&#xff09; Open报文已发送&#xff08;OpenSent&#xff09; Open报文已确认&#xff08;OpenConfirm&…

autodl 上 使用 LLaMA-Factory 微调 中文版 llama3

autodl 上 使用 LLaMA-Factory 微调 中文版 llama3 环境准备创建虚拟环境下载微调工具 LLaMA-Factory下载 llama3-8B开始微调测试微调结果模型合并后导出vllm 加速推理 环境准备 autodl 服务器&#xff1a; https://www.autodl.com/console/homepage/personal 基本上充 5 块钱…

前端框架-echarts

Echarts 项目中要使用到echarts框架&#xff0c;从零开始在csdn上记笔记。 这是一个基础的代码&#xff0c;小白入门看一下 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" co…

什么可以替代iframe?

网页嵌套中&#xff0c;iframe曾几何时不可一世&#xff0c;没有其他更好的选择&#xff01; iframe即内联框架&#xff0c;作为网页设计中的一种技术&#xff0c;允许在一个网页内部嵌套另一个独立的HTML文档。尽管它在某些场景下提供了便利&#xff0c;但也存在多方面的缺陷…

可视化实验三 Matplotlib库绘图及时变数据可视化

1.1 任务一 1.1.1 恢复默认配置 #绘图风格&#xff0c;恢复默认配置 plt.rcParams.update(plt.rcParamsDefault)#恢复默认配置 或者 plt.rcdefaults() 1.1.2 汉字和负号的设置 import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"]"SimH…

表格内容高效拆分,自定义行数随心所欲,让数据处理更高效!

在信息化社会的今天&#xff0c;表格成为了我们处理数据、整理信息的重要工具。然而&#xff0c;当表格内容过于庞大时&#xff0c;如何高效地拆分表格内容成为了摆在我们面前的一大难题。传统的拆分方法往往耗时耗力&#xff0c;且难以满足我们个性化的需求。 首先&#xff0…

数据中台:企业数字化转型的桥梁_光点科技

在数字化日益深入企业核心运营的今天&#xff0c;数据中台作为一个关键的信息化架构&#xff0c;正逐渐崭露头角&#xff0c;成为连接企业各部门、优化数据管理和推动业务创新的重要桥梁。 一、数据中台&#xff1a;连接与整合的桥梁 数据中台的核心作用在于连接与整合。传统的…

Python快速入门-零基础也能掌握的编程技巧,基础方法和API整理

目录 前言 数据结构 数字 数学运算 随机数 字符串 列表 元组 字典 面向对象 JSON 文件操作 扩展 制作一个简易时钟 前言 环境什么就不在赘述&#xff0c;可以参考其他文章&#xff0c;也可以在线运行 CSDN在线运行地址&#xff1a;InsCode - 让你的灵感立刻落地…

4G,5G执法记录仪人脸识别、人脸比对使用说明

4G/5G执法记录仪或4G/5G智能安全帽&#xff0c;做前端人脸识别、人脸比对&#xff0c;采用了上市公司的成熟的人脸识别算法&#xff0c;需要支付LICENSE给算法公司&#xff0c;理论上前端设备支持30K的人脸库&#xff08;受设备运行内存限制&#xff09;。 4G/5G执法记录仪侧要…

docker私有仓库registry

简介 Docker私有仓库的Registry是一个服务&#xff0c;主要用于存储、管理和分发Docker镜像。具体来说&#xff0c;Registry的功能包括&#xff1a; 存储镜像&#xff1a;Registry提供一个集中的地方来存储Docker镜像&#xff0c;包括镜像的层次结构和元数据。 版本控制&…

基于 LlaMA 3 + LangGraph 在windows本地部署大模型 (三)

基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;三&#xff09; 大家继续看 https://lilianweng.github.io/posts/2023-06-23-agent/的文档内容 第二部分&#xff1a;内存 记忆的类型 记忆可以定义为用于获取、存储、保留以及随后检索信息的过程。人脑中有多…