Linux学习之进程三

目录

进程控制

fork函数

什么是写时拷贝

进程终止

mian函数的返回值

退出码

错误码

 exit()

进程等待

1.什么是进程等待?

2.为什么要进行进程等待?

3.如何进程进程等待?

wait,waitpid:

waitpid

进程替换

1.什么是进程程序替换?

execl

原理

其他exec族的函数

替换我们自己的程序


进程控制

fork函数

在此之前,我们基本已经了解到了fork函数,即用它来创建一个子进程,fork函数无参数,返回值类型pid_t,其次fork函数有两个返回值,在子进程中,返回0,创建失败返回-1,在父进程中,给父进程返回子进程的pid。即等于0,子进程,大于0,父进程,-1,创建失败。

这里重点提一下:写时拷贝

通常情况下,父子进程共享一份代码,父进程在不写入时,数据也是共享的,当一个进程要去往里面写入时,便以写时拷贝的方法各自一份副本。

如图,左边为父进程的代码与数据,子进程拷贝父进程的代码数据。右边为当子进程尝试要写入时,此时会重新申请空间,自己拷贝一份数据段副本,再往里面写,并且修改之前的映射关系。

什么是写时拷贝

其中再在父进程创建子进程时,会将自己的数据区(页表)读写权限改为只读,然后再创建子进程。这个过程用户是不知道的,而用户可能会去对某一批数据进行写入,而页表会因为权限此时会出错,操作系统此时会介入进行判断(是因为越界还是权限问题),重新申请内存写入,从而进行写时拷贝。

通过fork函数我们可以让子进程和父进程执行不同的代码,或者一个进程执行不同的程序。

进程终止

首先如何创建一个多进程呢?我们可以利用循环fork创建多个子进程,

 #include<stdio.h>
   #include<unistd.h>
   #include<stdlib.h>
   #define N 10
   
   typedef void (*callback_t)();
  void worker()
   {
     int num=10;
    while(num--)
    {
      printf("i am a child process,my pid is%d,ppid is%d,num:%d\n",getpid    (),getppid(),num);
     sleep(1);
   }
  }
  
  void creatsubproc(int n,callback_t cb)
  {
    int i;
    for(i=0;i<n;i++)
    {
      sleep(1);
      pid_t p=fork();
      if(p==0)
      {                                                                  
        //子进程的工作
        printf("creat %d chid process\n",i);
        cb();
        exit(0);
      }  
     }
  }
  int main()
  {
   creatsubproc(N,worker);//这里传入worker函数,即函数指针
   sleep(100);
   return 0;
  }

首先对于进程的运行,我们不关心,操作系统自己调度,我们创建的多进程,当子进程一个个运行完,就变成僵尸状态了。这里创建子进程时让自己才能恒运行自己worker时,我们可以单独写在外面,通过函数指针的方式,传参,这样以便于我们去修改进程数量以及worker内容。

这是我们再用ps指令监控我们的进程:

while :; do ps ajx| head -1&& ps ajx |grep myproc|grep -v grep;echo "---------------------------";sleep 1;done

 可以看到进程数量不断增多,且慢慢的从s->z状态,一个个终止了。

这里进程终止我们是通过调exit()函数,来实现进程终止,进程终止除了这种方式,也会正常终止,我们可以i用echo $?查看错误码。

对于进程终止,它的原理我们应该清楚,无非就是创建的pcb,页表等,终止时销毁了。可是对于进程终止我们需要了解它的应用:

mian函数的返回值

想了解进程终止的应用,首先我们先了解mian函数的返回值,首先我们知道我们一般写c/c++最后都是返回0,为什么呢?

首先当一个进程跑完,他的情况是怎样的呢?无非有三种情况:代码运行完毕,结果正确;代码运行完毕,结果不正确;代码异常终止;对于父进程,我们只看前两种。

在多进程环境中,父进程想知道子进程运行完结果是怎样的,而且该进程并没有任何的打印信息(我们人也无法观察得出),我们如何得知?

退出码

 对于main函数的返回值,其实就是一个退出码,0就表示的是运行成功-success,非0通常表示运行结果错误--failed,父进程通过子进程返回的退出码,知晓进程运行结果如何。

错误码

对于进程返回0,运行成功,没什么好说的,可是若是非0,运行不正确,但我们并不知道是哪里的问题,只知道进程运行有问题,那么是因为什么原因失败的?此时我们就需要错误码来告诉我们,因为非0表示错误,非0的数字有很多,我们就可以用他们表示不同的出错原因。

系统里会有一套字符串表示错误信息用来对应每个数字,通过错误码就可以知道运行结果及问题所在。而这个字符串错误信息就是strerror(c语言中的时候我们就接触过),我们通过一个循环来看看它的错误码对应的错误信息,并且有多少个:

int mian()
{
//我们也不知道多少,假定200
for(int i=0;i<200;i++)
{
  printf("%d:%s\n",i,strerror(i));
}
return 0;
}

 而在linux中,可以用$?查看上一次进程(指令)的错误码,?就如同环境变量一样,存放的是错误信息。

这些内置的错误码,如果没有有你想要的的错误说明,你也是可以自定义的,直接定义这样的字符数组

const char*err_string[]={"success","not find","write errorr"}

其下标就是对应的错误码。

对于我们的linux进程的异常,也是有对应的异常信息,对应的异常的错误码如下:

而我们的进程的错误也可以人工向他发出信号给进程从而实现异常:

 exit()

我们在上文知道了,进程直接退出可以调用exit(),那么关于exit到底是什么呢?早在学习c语言的时候,我们也许就接触过了,那么它实际是怎么用的呢?

 可以看到头文件和参数类型,这里的参数其实就是错误码(退出码),作用就是直接引起进程结束。

我们在意一段代码理解exit,进程退出:

 1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int func()
  5 {
  6   printf("call func function done!\n");
  7   return 11;
  8 }
  9 int main()
 10 {
 11   func();//调用func函数
 12    printf("i am a process,pid: %d,ppid: %d\n",getpid(),getppid());
 13    //直接退出进程 exit(0);
 14    //对于main函数中直接return返回的是进程的错误码
 15    //其他函数return,仅代表该函数结束
 16    //return 21;
 17    //现在我们不用return 来返回退出码
 18    //直接调用exit对应的退出码。,其效果等价return                                                                                                     
 19    exit(21);
 20 }                                                
~                                                    
~                                                    
~               

我们在这里将他的退出码给21,此时我们运行程序后,在看他的错误码与return是一样的:

现在我们知道只有在main函数中的return 的是退出码,在子函数的return 只是代表返回该函数的返回值,退出该函数。当我们在其他函数中直接调用exit,此时就会直接退出进程,不会运行下面的代码了。

   #include<stdio.h>
   #include<unistd.h>
   #include<stdlib.h>
   int func()
  {
     printf("call func function done!\n");
     // return 11;
     exit(12);                                                                                                                                            
   }
  int main()
  {
    func();//调用func函数
     printf("i am a process,pid: %d,ppid: %d\n",getpid(),getppid());
     exit(21);
  }

 可以看到直接退出了进程,退出码为在这个子函数调用的exit的退出码。

除了手册3,man 3 exit 查看到的这个exit,还有man _exit,手册2的一个进程退出函数,用法基本相同,效果也差不多。

 它们有一个区别我们可以用一行代码体现:

printf("hello linu");
printf("hello linu\n");

首先对于exit,在终止进程时会刷新缓冲区,比如第一行代码,在没遇到\n之前,除非强制flush,否则只有等到进程终止此时才会想屏幕打印出来。

对于_exit,还是不加\n的时候。我们运行第一句话,这里没有/n,也没flush,此时进程终止,直接就退出来,什么也没打印,故此_exit终止不刷新缓冲区。

而所谓的缓冲区是绝对不在操作系统里,在我们的c库里,因此_exit与exit是存在这样的区别的。

进程等待

1.什么是进程等待?

首先进程等待就是通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程

2.为什么要进行进程等待?

之前讲过,子进程退出,父进程如果不管不顾,就可能造成 僵尸进程 的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入, kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

1.解决子进程僵尸问题带来的内存泄漏 ---必须的

2.子进程对于自己的任务完成的怎么样父进程需要知道---通过进程等待的方式获取子进程退出的信息(退出码与信号编号)。 

3.如何进程进程等待?

首先聊凭借两个接口:

wait,waitpid:

 对于wait,waitpid都是wait 2手册里的,调用这两个接口,需要包含头文件两个,参数分别为退出码;pid,退出码,选项。

wait的作用是可以帮父进程等待任意一个子进程的退出

waitpid:利用waitpid返回是否成功退出,返回pid退出成功,返回-1退出失败。

我们用一个例子来看看wait的作用:

void worker()
 {
    int cnt=5;
    while(cnt--)
   {
      printf("i am a process my pid: %d,my ppid:%d\n",getpid(),getppid())    ;
      sleep(1);
   }
  }
 
 int main()                                                             
 {
    pid_t id=fork();
    if(id==0)
   {
     worker();
     exit(0);
   }else{
      //father
      sleep(10);
     pid_t rid=wait(NULL);
     if(id==rid)
     {
      //等待成功
     printf("wait success,pid:%d\n",getpid()); 
     }
    sleep(10);
     }
       return 0;
}

 刚开始五秒,子进程在运行,五秒后遇到exit直接退出(异常退出),此时状态为僵尸状态,我们之前说过,僵尸进程是无法杀掉的,必须要让父进程接收到子进程的退出信息才行,之后继续运行父进程里的,先休眠10秒,做一下区分,然后父进程中,我们直接调用wait,退出码暂时不设置为null,调用wait之后,可以看到僵尸状态的子进程直接没了(子进程被成功回收),只有父进程,在10秒后,父进程结束。

总的概括就是,父进程通过调用wait函数,来获得子进程的退出信息,然后释放子进程,如果没调用wait,获取不到退出信息,此时进程就无法被释放。 

当我们在执行等待时,如果子进程根本就没有退出,父进程就必须在wait时进行阻塞等待,直到自己僵尸进程时候,wait就会自动回收,返回。也就是需要等待子进程运行完成为僵尸进程才能回收。

一般而言,谁先运行我们是不知道的,但是父进程都是最后退出的。

waitpid

wait只能等待当前的进程,没得选,而waitpid可以指定等待的进程。而对于waitdpid我们一般只需要掌握两种参数,首先对于Pid,指定等待进程的Pid,也可以设置为 -1,表示等待任意一个子进程。

参数int *status这里是一个输出型参数(通过函数把这个参数带出来给给操作系统),这里的option默认设置为0,表示的是阻塞等待。

我们将上述的测试再做修改:

 void worker()
 {
  int cnt=5;
  while(cnt--)
  {
 printf("i am a process my pid: %d,my ppid:%d--%d\n",getpid(),getppid(),cnt);
 sleep(1);
  }
 }
 
 int main()
 {
   pid_t id=fork();
  if(id==0)
 {
 worker();
 exit(10);
   }else{
      //father
     printf("wait before\n");
     int status=0;
    pid_t rid=waitpid(id,&status,0);
     if(id==rid)
    {
      //等待成功
      printf("wait success,pid:%d,%d\n",getpid(),status);
     }
     printf("wait after");                                                                                                                             
   }
  return 0;
 }

利用同等方式我们也可以实现进程等待,只不过对于这里的status,为什么时2560我们还是不得而知,实际上status是一个整数,一共32位,低16位:

首先我们在waitpid时不能对status整体使用,且我们可以通过等待完毕后的status的二进制分析得出他的退出码。

其次我们还可以通过有意8位在与上0xFF,得出信号位,用来表示是否收到信号。

对于这里的option除了0,一阻塞方式等待,还有一个状态NOHANG(宏),以非阻塞的方式的等待,

总结:

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候 waitpid 返回收集到的子进程的进程 ID
如果设置了选项 WNOHANG, 而调用中 waitpid 发现没有已退出的子进程可收集 , 则返回 0
如果调用中出错 , 则返回 -1, 这时 errno 会被设置成相应的值以指示错误所在;
参数:
pid
Pid=-1, 等待任一个子进程。与 wait 等效。
Pid>0. 等待其进程 ID pid 相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
WEXITSTATUS(status): WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进 程的ID

进程替换

1.什么是进程程序替换?

之前说过,父进程创建的子进程,子进程的代码和数据都是拷贝父进程的,可是我们如何让紫禁城区执行全新的任务呢,访问全新的数据,不在和父进程有瓜葛。且不是再去创建一个子进程。

这是我们需要程序替换:

fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数 以执行另一个程序。当进程调用一种exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动 例程开始执行。调用exec 并不创建新进程 , 所以调用 exec 前后该进程的 id 并未改变。

我们以一个父进程为例

首先了解一下名来进程替换的接口:exec函数族

我们先看下一下execl函数的调用

#include<stdio.h>
   #include<unistd.h>
   int main()
   {
     //我们知道我们所使用的命令行指令都是一个个程序
     printf("pid:%d,exec command begin\n",getpid());
     execl("/usr/bin/ls","ls","-a","-l",NULL);
     printf("pid:%d,exec command end\n",getpid());
     return 0;                                                                                                                                           
  }


我们写一个简单的代码,可以看到如上我们是没有常见任何进程的,但是当我们运行之后

 可以看到我们直接调起了指令ls -a -l。但是后面的那一句话没有打印出来

以同样的方式,我们还调用了top指令。

 #include<stdio.h>
 #include<unistd.h>
  int main()
   {
     //我们知道我们所使用的命令行指令都是一个个程序
     printf("pid:%d,exec command begin\n",getpid());
     execl("/usr/bin/top","top",NULL);                                    
     printf("pid:%d,exec command end\n",getpid());
    return 0;
  }

 从上述的结果看出,我们可以通过语言调用其他程序。

execl

总结上述,那么通过execl函数可以调用其他程序。调用完之后,后面的代码不在运行。

 execl函数就是其中一个可以替换程序的函数:

对于它的参数,第一个path,表示表示替换程序所在路径+文件名,第二个 const char*arg   以及后面的......其实是可变参数列表,都是表示如何使用该指令。

在此之前我们学习到的命令行参数中参数就与这里的可变参数本质上就是同一个。

即第一个参数找到该程序,后面的参数如何执行该程序(与命令行保持一致)。

注意:无论如何去传递参数,末尾一定是以NULL结尾,表示参数传递完毕!

原理

第二个问题,为什么执行完调用的程序,后面没在执行了?

实际上execl执行完就已经完成了程序的替换,我们知道在mm_struct中管理着进程的内存空间:

直接替换源程序的数据与代码,我们源程序的pid等其他属性不变,此时在这个过程中,不用产生新的进程就完成了进程的程序替换。

此时我们再通过多进程再来感受一下进程替换:

 #include<stdio.h>
   #include<unistd.h>
   #include<sys/types.h>
   #include<sys/wait.h>
   int main()
   {
     pid_t id=fork();
     if(id==0)
     {
     //子进程     
   printf("pid:%d,exec command begin\n",getpid());
   sleep(1);
    execl("/usr/bin/ls","ls","-a","-l",NULL);
    printf("pid:%d,exec command end\n",getpid());
   }else{                                                               
     //父进程
     pid_t rid=waitpid(-1,NULL,0);//父进程来等待子进程
    if (rid>0)
       {
         printf("wait succees rid:%d\n",rid);
       }
    }
 return 0;
 }

 看到结果首先pid是没有变化的,其次还是没有看到command end这句话。

创建子进程的时候,之前我们就说过,子进程与父进程数据共享,代码以写时拷贝的方法各自私有一份,在子进程发生替换时,通过写时拷贝(代码段与数据都重新拷贝),以保持父子的独立性,父进程与子进程不会相互影响。

现在就说一说子进程在替换后,是如何知道我们的代码段该从哪里运行,其次为什么的后面的代码不再执行了?

这个我们之前也提到过(fork创建子进程时运行代码段),其实就是程序计数器,pc指针与 eip会记录函数在运行的过程中,执行到哪一步了,在没替换之前,被记录下来,所以知道从哪里运行,其次,无论是多进程还是单进程,进程替换之后,代码都被替换,eip重新从新的程序开始,下面的代码就不会再执行了,最后被回收。

其他exec族的函数

了解了替换的本质后,我们再来看看exec族的其他函数:

 第一个参数与execl不一样,这里表示的是文件名,即这里的execlp,p指的是path,我们不需要给他完整的路径,他自己会去寻找,给出文件名即可。

  int main()
  {
    printf("pid:%d,exec command begin\n",getpid());
    execlp("ls","ls","-a","-l",NULL);                                    
    printf("pid:%d,exec command end\n",getpid());
    return 0;                                    
  }        

 

再看看exev,这里的vector表示的是vector,技术组,可以看到这里的第二个参数由可变参数变成了字符串数组传参,与我们之前说的命令行参数的形式一样,这里再传入参数时,提前将命令行参数传入数组:

int main()
    {
      printf("pid:%d,exec command begin\n",getpid());
      char*const argv[]={"ls","-a","-l",NULL};                          
      execv("/usr/bin/ls",argv);
      printf("pid:%d,exec command end\n",getpid());
      return 0;
    }

execvp可以看到这里就是与execv的区别就是不用完整路径(也可以用完整路径)。

int main()
    {
      printf("pid:%d,exec command begin\n",getpid());
      char*const argv[]={"ls","-a","-l",NULL};                          
      execv("ls",argv);
      printf("pid:%d,exec command end\n",getpid());
      return 0;
    }

替换我们自己的程序

基本掌握了程序替换,对于上述我们都是举例外壳程序的指令,当然我们是可以替换自己的程序:

我们写一个简单的c++代码--打印hello c++,编译形成可执行,通过execl调用我们的程序:

int mian()
{
execl("./myprocc","myprocc",NULL);
return 0;
}                                   

 

当然除了c++,python,汇编程序,其他脚本语言都可以替换,调用。

 

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

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

相关文章

Lua更多语法与使用

文章目录 目的错误处理元表和元方法垃圾回收协程模块面向对象总结 目的 在前一篇文章&#xff1a; 《Lua入门使用与基础语法》 中介绍了一些基础的内容。这里将继续介绍Lua一些更多的内容。 同样的本文参考自官方手册&#xff1a; https://www.lua.org/manual/ 错误处理 下…

node插件MongoDB(四)—— 库mongoose 操作文档使用(新增、删除、更新、查看文档)(二)

文章目录 前言&#xff08;1&#xff09;问题&#xff1a;安装的mongoose 库版本不应该过高导致的问题&#xff08;2&#xff09;重新安装低版本 一、插入文档1. 代码2. node终端效果3. 使用mongo.exe查询数据库的内容 二、删除文档1. 删除一条2. 批量删除3. 代码 三、修改文档…

Go基础知识全面总结

文章目录 go基本数据类型bool类型数值型字符字符串 数据类型的转换运算符和表达式1. 算数运算符2.关系运算符3. 逻辑运算符4. 位运算符5. 赋值运算符6. 其他运算符运算符优先级转义符 go基本数据类型 bool类型 布尔型的值只可以是常量 true 或者 false。⼀个简单的例⼦&#…

MIPSsim模拟器 使用说明

&#xff08;一&#xff09; 启动模拟器 双击MIPSsim.exe&#xff0c;即可启动该模拟器。模拟器启动时&#xff0c;自动将自己初始化为默认状态。所设置的默认值为&#xff1a; u所有通用寄存器和浮点寄存器为全0&#xff1b; u内存清零&#xff1b; u流水寄存器为全0&#xff…

C++结构体定义 创建 赋值 结构体数组 结构体指针 结构体嵌套结构体

结构体是什么&#xff1f; struct是自定义数据类型&#xff0c;是一些类型集合组成的一个类型。结构体的定义方式 #include<iostream> using namespace std;struct Student {string name;int age;int score; };创建结构体变量并赋值 方式一&#xff0c;先创建结构体变…

基于springboot+vue开发的教师工作量管理系

教师工作量管理系 springboot31 源码合集&#xff1a;www.yuque.com/mick-hanyi/javaweb 源码下载&#xff1a;博主私 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了教师工作量管理系统的开发全过程。通过…

人工智能技术的高速发展,普通人如何借助AI实现弯道超车?

人工智能技术的高速发展&#xff0c;普通人如何借助AI实现弯道超车&#xff1f; 随着互联网信息传播的爆炸&#xff0c;人类科技文明的快速发展“人工智能”成为新的话题&#xff0c;科技的进步也让普通人觉得自己与社会脱节&#xff0c;找工作越来越难&#xff0c;创业越来越难…

Python使用Numba装饰器进行加速

Python使用Numba装饰器进行加速 前言前提条件相关介绍实验环境Numba装饰器进行加速未加速的代码输出结果 numba.jit加速的代码输出结果 前言 由于本人水平有限&#xff0c;难免出现错漏&#xff0c;敬请批评改正。更多精彩内容&#xff0c;可点击进入Python日常小操作专栏、Ope…

Aspose.OCR for .NET 2023Crack

Aspose.OCR for .NET 2023Crack 为.NET在图片上播放OCR使所有用户和程序员都可以从特定的图像片段中提取文本和相关的细节&#xff0c;如字体、设计以及书写位置。这一特定属性为OCR的性能及其在扫描遵循排列的记录时的功能提供了动力。OCR的库使用一条线甚至几条线来处理这些特…

什么是证书管理

在自带设备和物联网文化的推动下&#xff0c;数字化使连接到互联网的设备数量空前加速。在企业网络环境中&#xff0c;每个在线运行的设备都需要一个数字证书来证明其合法性和安全运行。这些数字证书&#xff08;通常称为 X.509 证书&#xff09;要么来自称为证书颁发机构 &…

长虹智能电视使用123

1、开机 在接通电源的情况下&#xff0c;长虹智能电视开机有两种方式。 方式1&#xff1a; 按电视右下角开机按钮 方式2&#xff1a; 按电视遥控器开机按钮 长虹智能电视开机后会进入其操作系统&#xff08;安卓&#xff09;。 屏幕左右双箭头图表&#xff0c;手指点击会…

力扣876:链表的中间结点

力扣876&#xff1a;链表的中间结点 题目描述&#xff1a; 给你单链表的头结点 head &#xff0c;请你找出并返回链表的中间结点。 如果有两个中间结点&#xff0c;则返回第二个中间结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[3,4,5]…

无线优化之RRM模板

一、简介 RRM即,Radio Resource Management,射频资源管理 WLAN技术是以射频信号(如2.4G/5G的无线电磁波)作为传输介质,无线电磁波在传输过程中因周围环境导致无线信号衰减,从而影响无线用户上网的服务质量。 RRM模板主要用于保持最优的频射资源状态,自动检查周围无线…

二十四、城市建成区提取结果制图——建成区出图

一、前言 其实制图这一系列文章主要是为了照顾初学者,因为很多初学者并不是特别熟悉GIS平台一些操作,可能对于初步的制图有一定了解,但是对于一些稍微看起来高级并且复杂一点的图如何制作?例如下面这种多景的制作,其实吧万变不离其宗,这种仅仅只是拼接多幅数据框在一起,…

关于css 推荐几个超好看渐变色!

1.多彩糖果渐变 background: linear-gradient(135deg, #ff00cc, #ffcc00, #00ffcc, #ff0066);这个渐变色使用了多个鲜艳的颜色&#xff0c;从紫红色 (#ff00cc) 渐变到橙色 (#ffcc00)&#xff0c;然后到青色 (#00ffcc)&#xff0c;最后到鲜艳的粉红色 (#ff0066)。它给人一种快乐…

为啥$p(w|D)=p(y|X,w)$?

为啥 p ( w ∣ D ) p ( y ∣ X , w ) p(w|D)p(y|X,w) p(w∣D)p(y∣X,w)&#xff1f; p ( w ∣ X , y ) p ( w ∣ D ) p(w|X,y)p(w|D) p(w∣X,y)p(w∣D), p ( w ∣ D ) p ( D , w ) / p ( D ) p(w|D)p(D,w)/p(D) p(w∣D)p(D,w)/p(D)为啥 p ( D ∣ w ) p ( y ∣ X , w ) p(D|…

kubernetes istio

目录 一、部署 二、部署示例应用 三、部署遥测组件 四、流量管理 五、熔断 官网&#xff1a;https://istio.io/latest/zh/about/service-mesh/ 一、部署 提前准备好文件 tar zxf 15t10-1.19.3-linux-amd64.tar.gz cd 15t10-1.19.3/ export PATH$PWD/bin:$PATHistioctl install …

强化学习 - DQN及进化过程(Double DQN,Dueling DQN)

1.DQN 1.1概念 DQN相对于Q-Learning进行了三处改进&#xff1a; 1.引入神经网络&#xff1a;如下图所示希望能从状态S中提取Q(s,a) 2.经验回放机制&#xff1a;连续动作空间采样时&#xff0c;前后数据具有强关联性&#xff0c;而神经网络训练时要求数据之间具有独立同分布特性…

数据结构:AVL树的旋转(高度平衡树)

1、AVL树简介 AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1&#xff0c;所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis&#xff0c;他们…

CSS3渐变颜色

CSS3 渐变可以让你在两个或多个指定的颜色之间显示平稳的过渡。 CSS3渐变有两种类型&#xff1a;线性渐变&#xff08;Linear Gradients&#xff09;和径向渐变&#xff08;Radial Gradients&#xff09;。 线性渐变&#xff08;Linear Gradients&#xff09;&#xff1a; 线性…