【hello Linux】进程信号

news2024/11/26 22:51:05

目录

1. 进程信号的引出及整体概况

2. 信号的产生

1. 键盘产生

2. 进程异常

3. 系统调用

4. 软件条件

3. 信号的保存

1. 信号相关的常见概念

2. sigset_t

3. 信号集操作函数

4. sigprocmask:对block位图的操作

5. sigpending:对pending位图的操作

6. 捕捉信号

4. 信号的处理


Linux🌷 

1. 进程信号的引出及整体概况

在生活中关于信号的场景有很多。比如:早上响起的闹钟、过马路时的红绿灯、烽火台的烽火、跑步时的信号枪......这些信号都是在生活中的,是给人看的;

虽然这些场景还未到来时,但我们立马便能想到我们接下来应该做什么,其本质也就是:对于信号的处理动作,远远早于信号的产生,这是我们在长期以来积累的经验,又或者是通过学习知道的;

在计算机中也存在着信号,这个信号是OS给进程发送的,目的是为了让进程在合适的时候执行对应的动作。进程也是在没有收到信号时就能够识别信号并知道如何处理它,这是曾经编写OS的工程师在写进程源代码的时候就设置好的;

总结一下:进程具有识别信号并处理信号的能力,这是远远早于信号的产生的。

在生活中,我们收到某种”信号“的时候,并不一定是立即处理的:因为信号随时可能产生,我们在此时可能有更重要的事要做。也就是说:信号的产生和信号的处理是异步的。进程收到某种信号的时候,并不是立即处理的,而是在进程收到信号之后,先将信号保存起来,以供在合适的时候再进行处理。信号本质上也是数据,信号在发送之后,由OS将信号数据写往进程的task_struct中。信号产生的方式不止一种,无论信号时如何产生的,本质在底层都是通过OS发送的!

接下来将从(信号的产生——>信号的保存——>信号的处理)三个方面书写本篇的博客;

请大家耐心看下去,定会收获不少;

开始正文了!!!🔮


2. 信号的产生

1. 键盘产生

我们写了一个每隔1秒钟往显示器输出 ”hello linux!" 的死循环代码;

程序运行起来后,往键盘中输入 ctrl c  发现程序终止,猜想是该进程收到什么信号的原因;

信号捕捉函数:

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

我们可以在程序中,通过使用如上函数对信号进行捕捉;

其本质是修改进程对信号的默认处理动作;

signum:捕捉到的信号;

handler:对于信号的自定义处理方法;

对于信号捕捉函数,只有到收到信号时,才会执行 handler 方法;

#include <stdio.h>    
#include <signal.h>    
#include <unistd.h>    
    
//自定义处理方法    
void handler(int signo)    
{    
  printf("get a signal: signal no:%d\n",signo);    
}    
    
int main()    
{    
  //信号捕捉函数    
  int i;    
  for(i=1;i<31;i++)    
  {    
    signal(i,handler);    
  }    
  while(1)    
  {    
    printf("hello linux! pid:%d\n",getpid());                           
    sleep(1);    
  }    
  return 0;    
}    

 我们使用如上代码对 1~31 号信号 进行捕捉,发现从键盘输入 ctrl c 后确实产生了信号,并且是2号信号,同时我们也发现 9 号信号是不可被捕捉的;

查看系统定义的信号列表

kill -l

这些信号其实都是利用宏定义出来的,我们既可以使用信号前面的数字,又可以使用信号宏;

编号 34 以上的是实时信号,在此只讨论编号 34 一下的信号;

这些信号各自在什么条件下产生,默认处理动作是什么,我们都可以使用如下命令进行查看:

man 7 signal

在此只截出文件中的一小部分; 

注意:这些信号的默认处理动作都是终止进程,且只能用来终止前台进程;

使用 kill 命令发送信号:

kill [-信号名] [进程ID]

2. 进程异常

 我们写了一个算术异常的代码,经过运行,发现程序崩溃,猜测是否是因为收到信号的原因;

#include <stdio.h>    
#include <signal.h>    
#include <sys/types.h>    
#include <unistd.h>    
      
void handler(int signo)    
{    
  printf("get a signal! signal no:%d pid:%d\n",signo,getpid()  );        
  sleep(1);                                                  
}    
      
int main()    
{    
  //信号捕捉    
  int i;    
  for(i=1;i<31;i++)    
  {    
    signal(i,handler);    
  }    
  //算术异常    
  int a=10;    
  a/=0;    
  return 0;    
}    

上述代码对信号捕捉,发现确实是因为收到 8 号信号的原因;

这是因为CPU对上述代码中的异常算术进行了计算,导致CPU出现一些标志位的错误,OS作为硬件资源的管理者,要对硬件的 “健康” 负责,因此向该进程发送信号,达到终止进程的目的;

core dump:

在学习父进程 waitpid 等子进程时,有这样一张图;

 我们学习了进程退出时的退出码((status>>8)&0xff),进程退出时的信号(status&0x7f);

一直没有说 core dump 标志位是干什么的;

core dump:在程序异常终止时,如果有必要OS会将 core dump 标志位设为1,并将进程在内存中的数据转储到磁盘中,方便我们后期调试;

Linux云服务器,core dump 技术默认是关闭的,可以使用 ulimit -a 查看;

设置 core dump 大小:ulimit -c 10240

设置 core dump 前后对比: 

并且我们发现多了 core 文件,这个文件是二进制的,是OS把内存数据直接转载到磁盘中的,是可供我们调试的;

 注意:我们要调试程序肯定是在 gdb 下调试的,那编译时便要在后面带 -g 选项;

我们来验证一下如果 core dump 了,那么 core dump 标志位会被置为1;

 程序异常时:

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

int main()
{
  if(fork()==0)
  {
    printf("I am a child...\n");
    int a=10;
    a/=0;                                                                                                         
  }

  int status=0;
  waitpid(-1,&status,0);
  printf("exit code:%d, exit signo:%d, core dump flag:%d\n",(status>>8)&0xff,status&0x7f,(status>>7)&0x1);
  return 0;
}

 运行后发现 core dump 标志位果然被置为1了;

 程序正常时: 

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

int main()
{
  if(fork()==0)
  {
    printf("I am a child...\n");
    int a=10;
    a/=1;                                                                                                         
  }

  int status=0;
  waitpid(-1,&status,0);
  printf("exit code:%d, exit signo:%d, core dump flag:%d\n",(status>>8)&0xff,status&0x7f,(status>>7)&0x1);
  return 0;
}

 core dump 标志位为0;

注意:并不是所有的 singnal 都会产生 core dump(在此不进行验证了);

core 文件的使用:

3. 系统调用

kill:可以给指定的进程发送指定的信号; 

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

//成功返回0,出错返回-1

raise:给当前进程发送指定信号(自己给自己发); 

#include <signal.h>

int raise(int sig);

//成功返回0,出错返回-1

abort:使当前进程接收到信号而异常终止;

#include <stdlib.h>

void abort(void);

//abort函数总会成功,所以没有返回值

三个函数的使用:

注意:如下代码并不能直接运行 ,只是示例下三个函数的使用方法;

#include <sys/types.h>                                            
#include <signal.h>                                               
#include <stdlib.h>                                               
                                                                  
static void Usage(const char * proc)                              
{                                                                 
  printf("Usage:\n\t %s signo who\n",proc);                       
}                                                                 
int mainint(int argc,char *argv[])                                
{                                                                 
  //使用手册                                                      
  if(argc!=3)                                                     
  {                                                               
    Usage(argv[0]);                                               
    return 1;                                                     
  }                                                               
                                                                  
  int signo = atoi(argv[1]);                                      
  int who = atoi(argv[2]);                                        
                                                                  
  //kill                                                          
  kill(who,signo);    
                                            
  //raise                                                         
  raise(signo);      
                                                 
  //abort                                                         
  abort();       
                                                                  
  printf("signo:%d, who:%d\n",signo,who);       
  return 0;                                    
}               

4. 软件条件

软件条件:通过某种软件(OS),来触发信号的发送,系统层面设置定时器,或者某种操作而导致条件不就绪等这样的场景下,触发的信号发送;

在进程间通信时:当读端不光不读,而且还关闭了读fd,写端一直在写,最终写进程会受到sigpipe(13),就是一种典型的软件条件触发的信号发送;

今天主要介绍 alarm 函数和 SLGALRM 信号;

设置闹钟:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

功能:设置一个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号,该信号的默认处理动作是终止当前进程;

参数:设置的秒数;

返回值:为0,或者是以前设定的闹钟时间还余下的秒数;

代码练习: 

如下代码:设置了一个3秒的闹钟,对返回值进行输出 ,并且捕捉信号;

#include <stdio.h>    
#include <signal.h>    
#include <unistd.h>    
    
void handler(int signo)    
{    
  printf("get a signal! signo:%d\n",signo);    
}    
int main()    
{    
  //设置闹钟-3s    
  int ret = alarm(3);    
    
  //对信号进行自定义捕捉    
  int i;    
  for(i=1;i<31;i++)    
  {    
    signal(i,handler);                                                 
  }    
  while(1)    
  {    
    printf("hello linux! ret:%d\n",ret);    
    sleep(1);    
  }    
  return 0;    
}    

运行代码之后发现:alarm函数的返回值为0, 在3秒后收到了一个 14 号信号;

查看发现:14号信号就是SIGALRM;

 取消闹钟:

alarm(0);

//返回值为上个闹钟剩余的秒数

 代码:1s之后我们取消闹钟,并对返回值进行输出打印;

#include <stdio.h>    
#include <signal.h>    
#include <unistd.h>    
    
int main()    
{    
  //设置闹钟    
  int ret = alarm(10);    
  while(1)    
  {    
    sleep(1);                                                                                     
    //取消闹钟    
    int res = alarm(0);    
    printf("返回值 ret:%d , 剩余值 res:%d\n", ret, res);    
  }    
  return 0;    
}    

 接下来我们统计一下 1s CPU的运算次数;

#include <stdio.h>    
#include <signal.h>    
#include <unistd.h>    
      
int count=0;    
                                                                                                  
void handler(int signo)    
{    
  printf("count:%d\n",count);    
}    
      
int main()    
{    
  //设置一个1s的闹钟    
  alarm(1);    
  //对闹钟后的信号进行捕捉    
  signal(14,handler);    
  while(1)    
  {    
    count++;    
  }    
  return 0;    
}    

 我们将代码修改一下,边计算count的值,边进行输出:

#include <stdio.h>          
#include <signal.h>          
#include <unistd.h>          
            
int count=0;          
            
int main()          
{          
  //设置一个1s的闹钟          
  alarm(1);          
  //对闹钟后的信号进行捕捉          
  while(1)                                                  
  {                                 
    count++;                        
    printf("count:%d\n",count);      
  }                                  
  return 0;                          
}        

我们发现这样统计的CPU在1s内的运算次数,比上个代码慢4个0不止,根本原因就是该程序涉及到外设的输出,其大大影响了CPU的速度;

3. 信号的保存

产生的信号,并不一定是立即处理的,OS发送给进程的信号,进程可以先保存在task_struct中;

我们在此讨论的是1~31号信号,task_struct 是用一个 uint32_t sigs 的位图结构对信号进行保存;

比如:

00000000 00000000 00000000 00000000

比特位的位置:代表的就是哪一个信号;

比特位的内容:代表是否收到了信号;

信号在内核中的表示示意图:

 对于信号的保存,并以是一张位图完成的,而是三张位图的共同的结果;

block表(信号屏蔽字):代表该信号是否被堵塞;

pending表:代表是否收到了该信号;

handler表:该表其实是一个函数指针数组,保存着对于信号处理方法函数的地址;

对于一个信号,只有block为0(未堵塞) pending为1(收到信号) 才会处理handler的方法,也就是对信号的处理;

对于信号的处理有三种方法:

1. 默认 SIG_DFL:其实是一个宏定义,表示((sighandler_t)0);

2. 忽略 SIG_IGN:其实是一个宏定义,表示((sighandler_t)1);

3. 自定义函数(signal自定义捕捉);

在此便对信号的发送有了深一步的理解:

产生信号后,OS给进程发送信号,其本质就是OS向指定进程的task_struct中pending位图写入比特位1,即完成信号的写入;

1. 信号相关的常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2. sigset_t

从上图来看 , 每个信号只有一个 bit 的未决标志 , 0 1, 不记录该信号产生了多少次 , 阻塞标志也是这
样表示的。因此,未决和阻塞标志可以用相同的数据类型 sigset_t 来存储 ,sigset_t 称为信号集 , 这个类
型可以表示每个信号的“有效” 无效 状态 , 在阻塞信号集中 有效 无效 的含义是该信号是否被阻
, 而在未决信号集中 有 效” 无效 ”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前
进程的信号屏蔽字(Signal Mask), 这里的 屏蔽 应该理解为阻塞而不是忽略。

3. 信号集操作函数

sigset_t 类型对于每种信号用一个 bit 表示 有效 无效 状态 , 至于这个类型内部如何存储这些 bit
依赖于系统实现(从左开始,还是从右开始), 从使用者的角度是不必关心的 , 使用者只能调用以下函
数来操作 sigset_ t 变量 , 而不应该对它的内部数据做任何解释,比如用printf 直接打印 sigset_t 变量是没
有意义的。
#include <signal.h>

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • 函数sigfifillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptysetsigfifillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回 0, 出错返回 -1
sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,
不包含则返回 0, 出错返回 -1

4. sigprocmask:对block位图的操作

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
//返回值:若成功则为0,若出错则为-1
//set:输入型,返回老的信号屏蔽字——block位图,如果不想要的话设为NULL
//oset:输出型,修改block
如果 oset 是非空指针 , 则读取进程的当前信号屏蔽字通过 oset 参数传出;
如果 set 是非空指针 , 则更改进程的信号屏蔽字;
参数how 指示如何更改;
如果 oset set 都是非空指针 , 则先将原来的信号屏蔽字备份到 oset , 然后根据set how 参数更改信
号屏蔽字;
假设当前的信号屏蔽字为 mask, 下表说明了 how 参数的可选值
如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞 , 则在 sigprocmask 返回前 , 至少将其中一
个信号递达。

5. sigpending:对pending位图的操作

#include <signal.h>

int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。
//调用成功则返回0,出错则返回-1。
//不对pending位图做修改,只是单纯的获取进程的pending位图,只能OS来修改

下面用刚学的几个函数做个实验:

代码1:

其功能是将2号和9号信号进行屏蔽;

#include <stdio.h>    
#include <signal.h>    
#include <unistd.h>    
    
int main()    
{    
  sigset_t iset, oset;    
    
  //清空老的和新的信号    
  sigemptyset(&iset);    
  sigemptyset(&oset);    
    
  //将2号信号添加    
  sigaddset(&iset,2);    
    
  //将9号信号添加    
  sigaddset(&iset,9);    
    
  //1.设置当前进程的屏蔽字    
  //2.获取当前进程老的屏蔽字    
  sigprocmask(SIG_SETMASK, &iset, &oset);    
    
  while(1)    
  {    
    printf("I am a process! pid:%d\n",getpid());                                         
    sleep(1);    
  }    
  return 0;    
}    

我们向该进程发送2号信号发现没有反应,发送9号信号发现进程终止;

这也说明了9号信号不仅不可被自定义捕捉,也不可被屏蔽;

代码2:

将2号信号进行堵塞,并输出堵塞之前和堵塞之后的pending位图;

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

void show_pending(sigset_t *set)
{
  printf("pid:%d curr process pending:",getpid());
  int i;
  for(i=1;i<=31;i++)
  {
    if(sigismember(set,i))
    {
      printf("1");
    }
    else 
    {
      printf("0");
    }                                                                                                              
  }
  printf("\n");
}

int main()
{
  sigset_t iset, oset;

  //清空老的和新的信号
  sigemptyset(&iset);
  sigemptyset(&oset);

  //将2号信号添加
  sigaddset(&iset,2);

  //1.设置当前进程的屏蔽字
  //2.获取当前进程老的屏蔽字
  sigprocmask(SIG_SETMASK, &iset, &oset);

  sigset_t pending;
  while(1)
  {
    //清空pending 
    sigemptyset(&pending);

    //pending输出型参数
    sigpending(&pending);

    show_pending(&pending);
    sleep(1);

  }
  return 0;
}

 2号信号被堵塞,所以pending中的2号信号位置一直为1;

代码3:

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

void handler(int signo)
{
  printf("%d号信号被传递了,已经处理完成!\n",signo);
}
void show_pending(sigset_t *set)
{
  printf("pid:%d curr process pending:",getpid());
  int i;
  for(i=1;i<=31;i++)
  {
    if(sigismember(set,i))
    {
      printf("1");
    }
    else
    {
      printf("0");
    }
  }
  printf("\n");
}

int main()
{
  sigset_t iset, oset;

  //清空老的和新的信号
  sigemptyset(&iset);
  sigemptyset(&oset);

  //将2号信号添加
  sigaddset(&iset,2);

  //1.设置当前进程的屏蔽字
  //2.获取当前进程老的屏蔽字
  sigprocmask(SIG_SETMASK, &iset, &oset);

  sigset_t pending;                                                                                                
  int count=0;

  while(1)
  {
    //清空pending 
    sigemptyset(&pending);

    //pending输出型参数
    sigpending(&pending);

    show_pending(&pending);

    sleep(1);
    count++;
    if(count==10)
    {
  
      sigprocmask(SIG_SETMASK, &oset, NULL);
  
      //2号信号的默认动作是终止进程,看不到现象
      printf("恢复2号信号,可以被传递了!\n");
    }

    //对2号信号进行捕捉
    signal(2,handler);
  }
  return 0;
}

 6. 捕捉信号

signal函数可以完成信号的自定义捕捉,sigaction函数也可以

#include <signal.h>

int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。

 sigaction:是一个结构体,在此我们使用它的  (*sa_handler)(int) 和  sa_mask 

返回值: 调用成功则返回 0, 出错则返回 -1;
参数:
  • signo:指定信号的编号;     
  • act:输入型参数,若act指针非空,则根据act修改该信号的处理动作;
  • oact:输出型参数,若oact指针非 空,则通过oact传出该信号原来的处理动作,不想要的话置为NULL;

  (*sa_handler)(int) 的使用:

代码: 完成对2号信号的自定义捕捉;

#include <stdio.h>    
#include <signal.h>    
#include <string.h>    
#include <unistd.h>    
    
void handler(int signo)    
{    
  printf("get a signal! signo: %d\n",signo);    
}    
int main()    
{    
  struct sigaction act;    
  memset(&act, 0, sizeof(act));    
    
  act.sa_handler = handler;    
    
  //本质是修改当前进程的handler函数指针数组特定内容    
  sigaction(2, &act, NULL);    
    
  while(1)    
  {    
    printf("I am a process! pid:%d\n",getpid());                      
    sleep(1);    
  }    
  return 0;    
}    
//忽略处理    
act.sa_handler = SIG_IGN;    
      
//默认处理                                                          
act.sa_handler = SIG_DEL;    
当然我们也可以设置默认处理和忽略处理;
 sa_mask  的使用:捎带屏蔽
当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回
时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被
阻塞到当前处理结束为止。 如果在调用信号处理函数时, 除了当前信号被自动屏蔽之外 , 还希望自动
屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢
复原来的信号屏蔽字。
#include <stdio.h>    
#include <signal.h>    
#include <string.h>    
#include <sys/types.h>    
#include <unistd.h>    
    
void handler(int signo)    
{    
  printf("get a signal! signo:%d\n",signo);    
}    
int main()    
{    
  struct sigaction act;    
    
  memset(&act, 0, sizeof(act));    
    
  act.sa_handler = handler;    
    
  sigemptyset(&act.sa_mask);    
                                                                      
  //将3号信号捎带屏蔽    
  sigaddset(&act.sa_mask, 3);    
    
  sigaction(2, &act, NULL);    
    
  while(1)    
  {    
    printf("I am a process! signo:%d\n",getpid());    
    sleep(1);    
  }    
  return 0;    
}    

4. 信号的处理

我们之前总说信号的产生和信号的处理并不是同步的,而是在信号产生后在合适的时机进行信号的处理;

这个合适的时机:进程从内核态切换回用户态的时候,进行信号检测与信号的处理;

先来科普一些知识:

OS也是软件,在开机的时候,便是将OS的代码和数据加载到内存中。每个进程有自己的一张页表,OS也有:系统级页表,这张页表是供所有进程共享的。进程的程序地址有3GB给用户使用,还有1GB是给OS使用的。当进程切换至内核态时,便能访问那1GB空间,每个进程都是如此,进程之间无论如何切换,我们能保证我们一定能够找到同一个OS,因为我们每个进程都有3~4的地址空间,使用同一张内核页表。

CPU中有CR3寄存器,当CR3寄存器为0时:代表OS,当CR3为1时:代表用户;

用户态使用的是用户级页表,只能访问用户数据和代码;

内核态使用的是内核级页表,只能访问内核级数据和代码;

所谓的系统调用:就是进程的身份转化成为内核,然后根据内核页表找到系统函数,执行函数;

在大部分情况下,实际上我们OS都是可以在进程的上下文中直接运行的,但为了保护OS的安全,只允许OS执行内核代码;

上述皆是较为感性的认识;

下面的是较为理性的认识:

 信号的处理过程:

 用一张图更好的记忆下:

坚持打卡!😀

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

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

相关文章

三分钟看懂Python分支循环规范:if elif for while

人生苦短&#xff0c;我用python 分支与循环 条件是分支与循环中最为核心的点&#xff0c; 解决的问题场景是不同的问题有不同的处理逻辑。 当满足单个或者多个条件或者不满足条件进入分支和循环&#xff0c; 这里也就说明这个对相同问题处理执行逻辑依据具体参数动态变化&…

智慧医疗服务平台有哪些优势?

对于引用了智慧医疗服务平台的机构来说&#xff0c;其优势体现在哪些方面呢&#xff1f; 一、提高医疗资源利用效率。 在传统的医疗模式中&#xff0c;医生需要耗费大量的时间和精力去处理病人的病历、诊断、治疗等问题。而在智慧医疗服务平台的支持下&#xff0…

稳定排序和不稳定排序

稳定排序和不稳定排序 稳定排序 插入排序、冒泡排序、归并排序、基数排序 其时间复杂度 直接插入排序 最好情况:初始有序,为O(n); 最坏情况:初始逆序,为O(n) 平均时间复杂度T(n) O(n) 折半插入排序 时间复杂度为O(n) 冒泡排序 最好时&#xff0c;基本有序&#xff0c;…

Python 科研绘图可视化(后处理)Matplotlib - 2D彩图

Introduction 科研可视化是将数据和信息转化为可视化形式的过程&#xff0c;旨在通过图形化展示数据和信息&#xff0c;使得科研工作者能够更好地理解和分析数据&#xff0c;并从中发现新的知识和洞见。科研可视化可以应用于各种领域&#xff0c;如生物学、物理学、计算机科学…

制造策略 ETO、MTO、ATO、MTS

ETO 按交货周期跨度从长到短来讲&#xff0c;首先就是 ETO&#xff0c;Engineer To Order – 面向订单设计、定制生产或特殊生产。 就是客户给的订单&#xff0c;你要生产的话&#xff0c;你之前的原产品改动很大&#xff0c;或者基本上用不上&#xff0c;要完全按照客户的要求…

虚机制

一、虚机制的引入 利用动态编联实现——虚函数来解决上述问题 二、虚函数 必须是实例方法&#xff0c;不能是类方法。 若基类中析构函数为虚函数&#xff0c;则派生类中的析构函数不论写不写virtual关键字都是虚函数。派生类中新的虚函数应尽量避免与基类中的虚函数重名。 三、…

java基础知识——24.泛型

这篇文章我们来讲一下java的泛型 目录 1.什么是泛型 1.1 泛型的概念 1.2 泛型的好处 1.3 粗看泛型集合的源码 2.泛型类 2.1 泛型类的定义 2.2 从泛型类派生子类 3.泛型接口 4.泛型方法 5.类型通配符 5.1类型通配符上限 5.2 类型通配符的下限 6.类型擦除 6.1无限…

HTML(三) -- 表单设计

目录 1. 基本语法 2. 表单控件 2.1 input控件 input 常用属性&#xff1a; input type的表单项&#xff1a; 2.2 select 控件 2.3 textarea控件 2.4 label 控件 为什么需要表单&#xff1f; 在我们网页中&#xff0c; 无论是提交搜索的信息&#xff0c;还是网上注…

线程池核心线程是如何保持住的?

概述 本文尝试回答以下几个问题&#xff1a; 1、核心线程池是如何保持住的&#xff1f; 2、当没有任务时&#xff0c;超过核心线程数的线程是如何回收的&#xff1f; 3、线程队列为什么必须是BlockingQueue&#xff0c;普通队列行不行&#xff1f; 背景知识 以下是一些背景知…

第十三章 移动和旋转(上)

移动和旋转是游戏对象最频繁地操作。我们上个章节简单介绍了Cube的移动和旋转。移动是修改transform的position属性&#xff0c;旋转是修改transform的eulerAngles&#xff08;欧拉角&#xff09;属性&#xff0c;两者属性值均可以使用Vector3向量来实现。需要大家注意的是&…

C#窗体将DGV控件中数据导入导出Excel表

目录 界面图&#xff1a; 效果视频&#xff1a; 一&#xff1a;将DGV数据导出到Excel表中 二&#xff1a;将Excel表数据导入到DGV中 三&#xff1a;界面全部代码 界面图&#xff1a; 效果视频&#xff1a; DGV数据导入导出Excel 一&#xff1a;将DGV数据导出到Excel表中 p…

私人影院 - 本地搭建Emby媒体库服务器并在外远程访问 「无需公网IP」

文章目录 1.前言2. Emby网站搭建2.1. Emby下载和安装2.2 Emby网页测试 3. 本地网页发布3.1 注册并安装cpolar内网穿透3.2 Cpolar云端设置3.3 Cpolar内网穿透本地设置 4.公网访问测试5.结语 1.前言 在现代五花八门的网络应用场景中&#xff0c;观看视频绝对是主力应用场景之一&…

用ChatGPT问DotNet的相关问题,发现DotNet工程师的前景还不错

本人最近费了九牛二虎之力注册了一个ChatGPT账号&#xff0c;现在就给大家分享一下&#xff0c;问一下关于.NET的问题&#xff0c;看看ChatGPT的AI功能具体如何&#xff1f; 一、C#跟其它语言比较的优势 回答&#xff1a; C#是一门编程语言&#xff0c;它是为 Microsoft 的 …

【Linux】动态库与静态库

目录 一、前言 二、静态库与动态库 三、生成静态库 1、生成原理 2、完整过程 3、总结 四、生成动态库 1、环境变量 2、建立软链接 3、配置文件 五、动态库的加载 1、动态库加载的过程 2、动态库地址的理解 3、补充内容 一、前言 关于动态库与静态库的一小部分前置…

TCP的粘包和拆包

UDP有数据边界&#xff0c;TCP是没有数据边界&#xff0c;是流协议。如何拆包&#xff0c;就要靠应用层来处理。 四层网络模型&#xff0c;消息在进入每一层时都会多加一个报头。mac头部记录的是硬件的唯一地址&#xff0c;IP头记录的是从哪来和到哪去&#xff0c;传输层头记录…

GPT 护理机器人 - 让护士的工作变简单

引子    书接上文《GPT接入企微应用 - 让工作快乐起来》&#xff0c;我把GPT接入了企微应用&#xff0c;不少同事都开始尝试起来了。有的浅尝辄止&#xff0c;有的刨根问底&#xff0c;五花八门&#xff0c;无所不有。这里摘抄几份&#xff1a; “帮我写一份表白信&#xff…

Github 的使用

3. Github 在版本控制系统中&#xff0c;大约90%的操作都是在本地仓库中进行的&#xff1a;暂存&#xff0c;提交&#xff0c;查看状态或者历史记录等等。除此之外&#xff0c;如果仅仅只有你一个人在这个项目里工作&#xff0c;你永远没有机会需要设置一个远程仓库。只有当你…

嗯,这个树怎么和往常不一样?

文章目录 前言一、二叉树的链式存储二、二叉树链式结构的实现二叉树的结构设计手动构建二叉树二叉树的前序遍历二叉树的中序遍历二叉树的后序遍历二叉树的层序遍历计算二叉树大小计算叶子节点个数计算二叉树高度计算第K层的节点个数查找某个值对应的节点二叉树的销毁 三、完整代…

全球首个存量手机直连卫星天地语音通话,打通了!

4月25日&#xff0c;美国卫星通信初创公司——AST SpaceMobile&#xff0c;宣布打通了全球首个天基蜂窝语音通话。 对于卫星通信乃至整个通信行业来说&#xff0c;这是一个重大新闻&#xff0c;非常值得关注。 去年&#xff0c;我们还只是实现了手机和卫星之间的双向短消息通信…

Page管理机制

Page页分类 Buffer Pool 的底层采用链表数据结构管理Page。在InnoDB访问表记录和索引时会在Page页中缓存&#xff0c;以后使用可以减少磁盘IO操作&#xff0c;提升效率 Page根据状态可以分为三种类型&#xff1a; - free page &#xff1a; 空闲page&#xff0c;未被使用 - …