【unix高级编程系列】信号

news2024/11/15 17:19:16

引言

以前对信号的理解,仅仅停留在main函数入口注册几个异常信号(SIGPIPESIGSEGVSIGFPE)处理函数。当捕获到异常时,将进程的堆栈进行打印,方便排查、定位问题。这一类问题我认为是利用linux系统的异常信号机制,提高开发效率;后续随着工作经验的增长,linux的信号,还可以有其它用途:

  • 业务上的触发。比如可以监听SIGUSER1SIGUSER2信号,表示触发某一业务。
  • 提高软件的健壮性。比如可以监听SIGTERM信号(reboot命令内部,回向所有进程发送)。在系统重启前,做一些关键资源的备份或处理。
  • 定时器功能。比如通过监听SIGALRM信号,可以让系统在指定时间间隔,通知进程去做周期任务。

当然肯定还有其它的使用场景,等待着我去了解,拓展。本文主要介绍linux信号的相关概念,以及工作中的注意事项,常见接口的使用方式。希望能给到您帮助。

信号的概念

每一个信号都有一个名字,他们都是以SIG开头,比如:SIGABRT是夭折信号;SIGALRM是闹钟信号;

注:信号名都是被定义为正整数常量。

信号是一个异步事件。因此当内核检测到它是触发时,实际上有两个做法:

  1. 设置一个变量(如signal),应用程序周期判断该变量状态,判断触发信号类型。
  2. 内核中断当前进程的执行代码块,去执行指定操作。

很明显方案一存在时效性的问题,因为信号的发生是可能在任一时刻的。

linux 内核处理信号的方式有三种:

  1. 忽略此信号。但是SIGKILLSIGSTOP信号无法忽略,因为需要向内核和超级用户提供使进程终止的或停止的可靠方法。也就是说我们常见的SIGSEGV段错误,实际也可以让内核忽略,从而让进程不退出。但是我们往往不会这么操作,因为一旦发生类型错误,说明代码或业务已经出现异常,无法保证正确可靠的运行了。
  2. 捕捉信号。即告诉内核捕捉到该信号后,需要调用一个用户函数。这也是我们常见的做法。
  3. 执行系统默认动作。大多数信号的系统默认动作是终止该进程。可参考下表。
信号说明默认动作
SIGABRT调用abort函数使,产生此信号终止+core
SIGALRM调用alarmsetitimer函数,产生此信号终止
SIGBUS硬件故障终止+core
SIGCHLD子进程终止或停止时,会将该信号发送给父进程,期望回收子进程资源忽略
SIGCONT若当前进程处于停止状态,则进行运行。否则忽略忽略/继续
SIGEMT硬件故障终止+core
SIGFPE算数运算异常。如除以0、浮点溢出等终止+core
SIGHUP终端检测到一个连接断开,则将该信号发送给会话中所有进程忽略
SIGILL执行一条非法硬件指令终止+core
SIGINT当用户按下中断键(ctrl+c),则将该信号发送前台进程组中的所有进程终止
SIGIO一个异步I/O事件终止/忽略
SIGIOT硬件故障终止+core
SIGKILL不可被捕捉。向系统管理员提供了可以终止任一进程的可靠方法终止
SIGPIPE如果在管道的读进程已经终止时写管道、当类型为SOCK_STREAM的套接字已不再连接时,进程写该套接字都会触发该信号终止
SIGPWR用于具有不间断电源(UPS)的系统终止/忽略
SIGQUIT当用户输入Ctrl+\时,终端驱动程序产生此信号,并发送给前台进程组中的所有进程终止+core
SIGSEGV引用无效的内存地址,也就是我们常说的:内存越界终止+core
SIGSTOP不可被捕捉。这是一个作业信号,停止一个进程终止
SIGSYS无效的系统调用终止+core
SIGTERM系统默认终止信号,比如执行reboot命令,会向所有进程发送该信号。用户进程可通过捕捉该信号,在进程退出前做好清理工作终止
SIGTSTP当用户输入Ctrl+Z挂起键时,终端驱动程序产生此信号,并发送给前台进程组中的所有进程终止
SIGUSER1这是用户定义的信号终止
SIGUSER2这是用户定义的信号终止

其中core文件:复制了该进程的内存映像,方便后续调试。关于如何调试core文件,可参考linux gdb 调试专栏。

修改系统对信号的处理方式

从上章节中内核对信号的处理方式有三种:忽略、捕捉、默认。我们可以通过signal函数进行设置。

#include <signal.h>
/**
 * @brief 信号注册处理函数
 * @details 
 *
 * @param [in] signo 信号值
 * @param [in] func 信号处理函数
 * @return     若成功,返回以前的信号处理配置;若出错,返回SIG_ERR
 * @note
 */
void (*signal(ing signo, void(*func)(int))) (int);

#define SIG_ERR (void (*)()) (-1) // 一般用于判断signal 接口是否成功
#define SIG_DFL (void (*)()) (0)  // 提示内核按照默认动作处理该信号
#define SIG_IGN (void (*)()) (1)  // 提示内核忽略该信号

注:我们常常仅关注signal返回值是否是SIG_ERR。但我觉得这是不充分的。

比如存在这样的场景:

你作为软件SDK的提供者,并且在内部捕捉了部分信号,方便用于进行调试或业务开发。但是SDK集成方,也注册了相关信号处理。那么就会出现竞争情况,导致意料之外的情况发生。

如下:

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

void handlerAlarm(int signo)
{
    printf("signo = %d\n", signo);
    return;
}

void handlersdkAlarm(int signo)
{
    printf("signo = %d\n", signo);
    exit(-1);
    return;
}

int init_sdk()
{
    if (signal(SIGALRM, handlersdkAlarm) == SIG_ERR)
    {
        printf("registerSIGALRM failed\n");
        return -1;
    }
}

int main()
{
    if (signal(SIGALRM, handlerAlarm) == SIG_ERR)
    {
        printf("registerSIGALRM failed\n");
        return -1;
    }
    alarm(5);

    /** 第三方SDK */
    init_sdk();

    while (1)
    {
        sleep(60);
    }

    return 0;
}

分析:原本进程针对SIGALRM信号的处理,仅是日志打印记录。但是SDK提供方则任务这是一个异常,退出进程。这很明显就修改了进程的本意。

我的建议按照以下流程:

  1. 判断signal返回值是否是系统默认,,若是系统默认则说明没有应用对该信号捕获。
  2. 若不为系统默认处理,则发出警告或恢复。
    逻辑大致如下:
    void* pftmp = signal(SIGALRM, handlerAlarm);
    if (pftmp == SIG_ERR)
    {
        printf("registerSIGALRM failed\n");
        return -1;
    }
    if(pftmp != SIG_DFL)
    {
        printf(" SIGALRM have register\n");
        abort(0);
    }

这是通过技术手段避免对唯一资源的竞争使用判断,最简单的方式,则是提前与集成方沟通约束。类似的还有进程的标准输入输出也存在类似竞争问题。

程序启动后,信号的处理方式。

在之前的进程控制章节中,我们知道进程创建的方式有两种:

  • fork函数族。

当一个进程调用fork时,其子进程集成父进程的信号处理方式;

  • exec函数族。

exec函数将原先设置为要捕捉的信号都更改为默认状态。

其原因是:exec启动一个程序,其原理是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。因此之前注册的信号处理函数地址,在当前不一定存在意义,因此需要恢复系统默认。而fork创建的子进程会将父进程的代码段都复制,因此,可以继承父进程的信号处理方式。

中断的系统调用

linux 系统中有一个特性是:如果一个进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用会被中断,不再继续执行。该系统调用返回错误,并将errno设置为EINIR

低速系统调用指的是可能会使进程永远阻塞的一类系统调用。包括:

  1. 如果某些类型行文件(如读管道、终端设备和网络设备)的数据不存在,则读操作(read,readv)可能会使调用者永远不会返回。
  2. pausewait函数。
  3. 某些ioctl函数。
  4. 某些进程间通信。
  5. 如果数据不能被相同的类型文件立即接受,则写操作(writewritev)可能会使调用者永远阻塞。

当我们知道低俗系统调用可能会被信号中断,那我们在编写代码时就需要增加相关防错。如下:

    /* 阻塞读取socket 数据*/
    int rByte = read(socket,buff,1024);
    if(rByte < 0)
    {
        /* 网络链接异常,断开重新连接*/
        close(socket); 
    }

上述代码似乎没有问题:当read出错时,则认为socket异常,重新建立连接。理论上功能都可以实现,但是稍微修改一下,我觉得可能会更好些。优化后版本:

    /* 阻塞读取socket 数据*/
    int rByte = read(socket,buff,1024);
    if(rByte < 0 && (errno != EINTR))
    {
        /* 网络链接异常,断开重新连接*/
        close(socket); 
    }

思考:现在的进程基本都是多线程任务,那么信号触发时,中断的是哪一个线程呢?

经过下列代码验证(sleep也可以被信号中断):

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>

void handlerSignal(int signo)
{ 
    printf("pid=%ld signo = %d\n",pthread_self(),signo);
    sleep(5);
    return;
}

void* thread_function(void* arg) 
{
    printf("thread_function pid=%ld\n",pthread_self());
    /** ALRM */
    alarm(5);

    /** abort */
    abort();
    
    /** SIGSEGV */
    strcpy(NULL,"123");
    while(1)
    {
        sleep(60);
        printf("thread_function sleep have broken\n");
    }
    return NULL;
}


int main()
{
    printf("main pid=%ld\n",pthread_self());
    if (signal(SIGALRM, handlerSignal) == SIG_ERR)
    {
        printf("registerSIGALRM failed\n");
        return -1;
    }
    if (signal(SIGABRT, handlerSignal) == SIG_ERR)
    {
        printf("registerSIGALRM failed\n");
        return -1;
    }
    if (signal(SIGSEGV, handlerSignal) == SIG_ERR)
    {
        printf("registerSIGALRM failed\n");
        return -1;
    }
    pthread_t pit;
    if(pthread_create(&pit,NULL,thread_function,NULL) != 0)
    {
        printf("create thread failed\n");
        return -1;
    }

    while (1)
    {
        sleep(60);
        printf("main sleep have broken\n");
    }

    return 0;
}
  • 进程外传入的信号(kill -signo pid),默认中断主线程。
  • 进程内部创建的信号,比如SIGALRM信号,也是中断主线程;但是SIGSEGVSIGABRT等信号中断的是触发的线程。

可重入函数

可重入函数必须要满足以下条件:

  1. 不使用静态或全局数据结构
  2. 不可以调用mallocfree
  3. 不可以是标准I/O

信号处理函数中保证调用可重入函数。否则对进程的影响是不可预估的。比如:进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号,信号处理函数中也调用了malloc,因为malloc通常为它维护了一个链表,信号处理函数中就修改了进程的链表。导致异常。这是因为早期的libc库中的malloc实现没有采用锁机制。即使加入了锁,信号处理函数也不建议调用,因为容易产生死锁。

线程安全和信号安全

有时候我们会接触到这两个概念,有时候傻傻分不清楚,容易在编码过程中造成一些隐患。

线程安全

线程安全是指一个函数可以被多个线程并发调用而不导致数据不一致或程序崩溃。线程安全函数应该满足以下条件:

  1. 不修改全局或静态数据,或者任何修改都必须要是原子操作或锁保护。
  2. 不返回指向静态数据的指针,或者确保指针在使用期间不会被修改。
  3. 只依赖于调用时的参数,不依赖于任何外部状态。

比如:

int g_count;
int countPlus()
{
    int count = g_count++;

    return count;
}

由于countPlus内部调用了全局变量,且没有用锁保护,因此它是线程不安全函数。可通过锁保证多线程安全:

pthread_mutex_t lock;
int g_count;
int countPlus()
{
    pthread_mutex_lock(&lock);  // 加锁
    int count = g_count++;
    pthread_mutex_unlock(&lock); // 解锁
    return count;
}

信号安全

信号安全是指一个函数可以在信号处理函数中被安全地调用,而不会导致未定义的行为。信号安全函数必须满足以下条件。

  1. 不调用任何非可重入函数:大多数可重入函数也是信号安全的。
  2. 不访问或修改全局或静态数据:除非这些数据是专门为信号处理而设计的,并且不受其他线程影响。

注:如上所示的线程安全函数countPlus,就不是信号安全函数。因为在执行int count=g_count++指令时,触发信号处理函数,并且信号处理函数中调用了countPlus接口,就会造成死锁。

SIGCLD信号用途

在【unix高级编程系列】进程控制中,我们介绍了子进程退出后,如果没有对其进行资源回收,则会产生僵尸进程,导致对系统资源造成影响。通常情况下,我们的做法是在父进程中调用waitpid等待子进程结束,并回收资源。这样的做法会导致父进程业务阻塞,并不是好的方式。

之后了解到子进程结束时,会向父进程发送SIGCLD信号,因此我们可以从该信号做文章。总体有两种方式:

  1. 默认忽略,子进程将不再产生僵尸进程。如:
int main() 
{
    signal(SIGCHLD, SIG_IGN);
}
  1. 在信号处理函数中回收子线程资源。如:
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void sigchld_handler(int sig) 
{
    int status;
    pid_t child_pid = wait(&status);
    if (child_pid > 0) {
        printf("子进程 %d 已退出,状态: %d\n", child_pid, status);
    }
}

int main() {
    signal(SIGCHLD, sigchld_handler); // 设置SIGCHLD的处理函数

    pid_t pid = fork();

    if (pid > 0) {
        // 父进程
        // ... 父进程可以继续其他工作
    } else if (pid == 0) {
        // 子进程
        printf("子进程开始执行...\n");
        sleep(1); // 模拟子进程工作
        printf("子进程结束。\n");
        exit(0); // 子进程退出
    } else {
        // fork失败
        perror("fork");
        exit(1);
    }

    return 0;
}

常用信号函数

kill和raise

killraise都是发送信号。kill函数将信号发送给指定的进程或进程组;而raise是发送线程自身;

#include <signal.h>
int kill(pid_t pid, int signo);

int raise(int signo);
    // 若成功返回0;若出错返回-1;

通过raise(signo)等价于kill(getpid(),signo)

其中kill的pid参数有以下4种情况:

  • pid>0;将该信号发送给进程ID为pid的进程;
  • pid==0;将信号发送给发送进程属于同一进程组的所有进程;
  • pid<0;将信号发送给其进程组ID等于pid绝对值;
  • pid==-1;将信号发送给发送进程有权限像它们发送信号的所有进程;

注:signo==0,常被用来判断一个进程是否仍然存在。

alarm和pause

使用alarm函数可以设置一个定时器,在将来的某个时间该定时器会超时,并产生SIGALRM信号。如果忽略或不捕捉此信号,则默认终止该进程

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
    //返回值: 0 或以前设置的闹钟时间的余留秒数

注: 每个进程只能有一个闹钟时钟;

pause函数是调用线程挂起,直至捕捉到一个信号。

#include <unistd.h>
int pause(void);
    //返回值:-1,errno 被设置为EINTR

注:pause仅是阻塞调用线程,并不会阻塞整个进程。如下示例:

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>


void* thread_function(void* arg) 
{
    int count = 0;   
    while(1)
    {
        sleep(3);
        printf("thread_function count=%d\n",count++);
        if(count == 3)
        {
            pause();
        }
    }
    return NULL;
}


int main()
{
    pthread_t pit;
    if(pthread_create(&pit,NULL,thread_function,NULL) != 0)
    {
        printf("create thread failed\n");
        return -1;
    }

    int count = 0;   
    while (1)
    {
        sleep(3);
        printf("main count=%d\n",count++);
    }

    return 0;
}

编译输出如下:

xieyihua@xieyihua:~/test$ gcc 6.c -o 6 -lpthread
xieyihua@xieyihua:~/test$ ./6
main count=0
thread_function count=0
main count=1
thread_function count=1
main count=2
thread_function count=2
main count=3
main count=4
main count=5
^C

sigaction函数

sigaction是检查或修改与指定信号相关联的处理动作。基本已取代了signal

#include <signal.h>
struct sigaction
{
    void (*sa_handler)(int); // 信号处理函数的地址、或SIG_IGN、或SIG_DFL
    sigset_t sa_mask; //是一个信号集,用于指定在处理信号时需要被阻塞的信号
    int sa_flag;    //saflags 是一些标志,用于改变 sigaction 的行为
    void (*sa_sigaction)(int , siginfo_t *,void*); //是另一个函数指针,用于更复杂的信号处理
};
int sigaction(int signo, 
                const struct sigaction *restrict act,
                struct sigaction *restrict oact);
    //返回值:若成功,返回0;若出错,返回-1

分析:

  • act指针非空,则要修改其动作。
  • oact指针非空,返回该信号的上一个动作。

常见用法:

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

void handle_sigint(int sig) {
    printf("Caught signal %d\n", sig);
}

int main() {
    struct sigaction sa;

    sa.sa_handler = &handle_sigint;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGINT, &sa, NULL);

    while (1) {
        printf("Hello, World!\n");
        sleep(1);
    }

    return 0;
}

sleep函数

sleep是我们工作中经常使用的函数,但是我相信它的一些注意事项很多人都不太了解。可能会造成问题;

#include <unistd.h>
unsigned int sleep(unisigned int seconds);
    //返回值:0或未休眠的秒数

sleep函数返回的场景有两种:

  1. 已经过了seconds所指定的墙上时钟时间。
  2. 调用进程捕捉到一个信号,并从信号处理程序中返回。

注:墙上时钟时间指实际的物理时间,即现实世界中的时间

其中第二点是我们常常会忽略,造成错误的。假设有如下代码:

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
int g_dothing = 0;

static void sig_alarm(int signo)
{
    alarm(5);
    g_dothing = 1;

    return;
}
void* thread_function(void* arg) 
{

    if(signal(SIGALRM,sig_alarm) == SIG_ERR)
    {
        printf("signal failed\n");
        return NULL;
    }
    alarm(5);
    while(1)
    {
        if(g_dothing == 1)
        {
            /**
             * TODO: 执行业务处理
             */
            printf("do someting\n");
            g_dothing = 0;
        }
        sleep(1);
    }
    return NULL;
}

int main()
{
    pthread_t pit;
    if(pthread_create(&pit,NULL,thread_function,NULL) != 0)
    {
        printf("create thread failed\n");
        return -1;
    }

    while (1)
    {
        sleep(60);
        /**
         * TODO: 上报心跳
         */
        printf("report heartbeat\n");
    }

    return 0;
}

分析:该进程有两个业务线程:

  1. 主线程周期60秒上报心跳;
  2. 子线程周期5秒,执行相关动作;

但实际运行过程中,主线程中的sleep会被子线程中定时器唤醒,因此主线程的上报周期变成了5秒。导致与预期不符。我们应该关注sleep函数的返回值,对主线程做以下优化:

   int unsleepTime = 60;
    while (1)
    {
        unsleepTime = sleep(unsleepTime);
        if(unsleepTime == 0)
        {
            /**
             * TODO: 上报心跳
             */
            printf("report heartbeat\n");
            unsleepTime = 60;
        }
    }

总结

本文详细介绍了Linux信号的概念、处理方式、常见信号的用途以及信号处理函数的使用。希望能给您带来帮助

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途
在这里插入图片描述

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

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

相关文章

系统架构师选择题知识点笔记

系统架构师复习笔记 选择题知识点 分布数据库特性 分片透明性&#xff1a;指用户或应用程序不需要知道逻辑上访问的表具体是怎么分块存储的&#xff1b;复制透明性&#xff1a;指采用复制技术的分布方法&#xff0c;用户不需要知道数据是复制到哪些节点上&#xff1b;逻辑透…

AI绘画网站爆款推荐,人人都是艺术家!

嗨&#xff0c;小伙伴们&#xff0c;最近AI 绘画简直是火出了新高度&#xff01;网上那么多 AI 绘图站&#xff0c;你或许已经眼花缭乱。但悄悄告诉你&#xff0c;有些站点只能照搬照片&#xff0c;却不让你天马行空地改造&#xff0c;想找一个既聪明又有创意的 AI 画家&#x…

Nginx详解配置实例及企业高性能web服务器

目录 企业高性能web服务器 一.Web 服务基础介绍 1.1 互联网发展历程回顾 1.2 Web 服务介绍 1.2.1 Apache 经典的 Web 服务端 1.2.1.1 Apache prefork 模型 1.2.1.2 Apache worker 模型 1.2.1.3 Apache event模型 1.2.2 Nginx-高性能的 Web 服务端 1.2.3服务端 I/O 流…

循环图神经网络教程2——循环图神经网络

介绍 循环图神经网络&#xff08;Recurrent Graph Neural Network&#xff0c;RGNN&#xff09;。在标准神经网络中&#xff0c;连续的学习权重层用于从输入张量中逐步提取更高级别的特征。在用于计算机视觉的神经网络中&#xff0c;低级特征(如短直线和曲线)的存在是由较早的…

Java类的卸载

类、类的加载器、类的实例之间的引用关系 类的生命周期 类的加载器如下&#xff1a;

进程部分相关概念

进程 进程是一个程序执行的过程&#xff0c;会去分配内存资源&#xff0c;cpu的调度 进程不仅仅是一个正在运行的程序&#xff0c;它包含了程序执行所需的所有资源和状态信息 资源分配&#xff1a;操作系统会为每个进程分配必要的资源&#xff0c;如内存空间、文件句柄等。这些…

nginx重定向与防盗链

重定向 由于网站的扩容&#xff0c;负荷较大&#xff0c;需要将一部分内容迁移到其他服务器上。但是这个时候又不能更改用户常访问的域名&#xff0c;因此对其进行重定向。 1. 域名重定向 www.sxl1.com/aaa的站点需要重定向到aaa.sxl1.com rewrite ^/aaa$ aaa.sxl1.com/…

letcode 分类练习 513.找树左下角的值 112. 路径总和 106.从中序与后序遍历序列构造二叉树

letcode 分类练习 513.找树左下角的值 112. 路径总和 106.从中序与后序遍历序列构造二叉树 513.找树左下角的值112. 路径总和106.从中序与后序遍历序列构造二叉树 513.找树左下角的值 遍历二叉树&#xff0c;并记录当前的深度&#xff0c;如果深度大于最大深度&#xff0c;那么…

关于 Vivado HLS 的三大误读

【转载】关于 Vivado HLS 的三大误读 目前&#xff0c;在高层次综合&#xff08;HLS, High Level Synthesis&#xff09;领域&#xff0c;Vivado HLS可谓独树一帜。它有效地拉近了软件工程师与FPGA之间的距离&#xff0c;加速了算法开发的进程&#xff0c;缩短了产品上市时间。…

leetcode_52. N 皇后 II

52. N 皇后 II 题目描述&#xff1a;n 皇后问题 研究的是如何将 n 个皇后放置在 n n 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回 n 皇后问题 不同的解决方案的数量。 示例 1&#xff1a; 输入&#xff1a;n 4 输出&#xff1a;2…

C++:病毒系列回归记2/3 (Doge智能系统已上线)

上一期&#xff1a;C:病毒系列回归记1/3 (Doge智能系统已上线) 这一步真的非常爽哦 void Crazy(int n) {if(n 0)system("start ラム.vbs");if(n 1)system("start 今年のトラック.vbs");if(n 2)system("start ロシアのスートン焼却発電.vbs")…

安装并配置开发环境

安装并配置开发环境 获取虚拟机系统 下载vmware虚拟机工具 使用浏览器打开网址 https://www.vmware.com/products/workstation-pro/workstation-pro-evaluation.html 参考下图箭头所示&#xff0c;点击下载安装 Windows版本的VMware Workstation &#xff0c;点击 DOWNLOAD …

java基础--day10字符串

视频网址&#xff1a;字符串-01-API和API帮助文档_哔哩哔哩_bilibili 1.API 1.1API概述 什么是PAI API (Application Programming Interface) &#xff1a;应用程序编程接口 java中的API 指的就是 JDK 中提供的各种功能的 Java类&#xff0c;这些类将底层的实现封装了起来&am…

MySQL笔记01: MySQL入门_1.3 MySQL启动停止与登录

1.3 MySQL启动停止与登录 1.3.1 MySQL启动与停止 MySQL数据库分为客户端和服务器端&#xff0c;只有服务器端服务开启以后&#xff0c;才可以通过客户端登录MySQL服务端。 首先&#xff0c;以管理员身份运行“命令提示符”&#xff1a; &#xff08;1&#xff09;启动MySQL服务…

AArch64中的寄存器

目录 通用寄存器 其他寄存器 系统寄存器 通用寄存器 大多数A64指令在寄存器上操作。该架构提供了31个通用寄存器。 每个寄存器可以作为64位的X寄存器&#xff08;X0..X30&#xff09;使用&#xff0c;或者作为32位的W寄存器&#xff08;W0..W30&#xff09;使用。这两种是查…

PyTorch——transforms

接着上一篇&#xff0c;我们这一篇讲transforms 1、什么是transform 首先transform是来自PyTorch的一个扩展库——【torchvision】&#xff0c;【torchvision】这个库提供了许多计算机视觉相关的工具和功能&#xff0c;能够在神经网络中&#xff0c;将图像、数据集、预处理模型…

[Winform] Chart获得当前点的X和Y值

在利用C#控件绘制曲线图后&#xff0c;有时我们需要通过鼠标查看数据点的值信息&#xff0c;常用的方法就是利用chart控件的chart1_GetToolTipText(object sender, ToolTipEventArgs e)事件来获取数据点的信息&#xff0c;如下我用两个label来显示获取的数据点的值 相关代码如下…

【手撕数据结构】链式二叉树

目录 链式二叉树的结构及其声明链式二叉树的四种遍历方式前序遍历中序遍历&#xff08;中根遍历&#xff09;后序遍历层序遍历概念思路分析详细代码 求树的节点个数变量累加法(错误)分治递归法 求树的叶子节点个数警惕空指针正确代码 求第k层节点个树思路分析及规则明细代码详细…

POK´ELLMON:在宝可梦战斗中实现人类水平的人工智能

人工智能咨询培训老师叶梓 转载标明出处 最近&#xff0c;由美国乔治亚理工学院的Sihao Hu、Tiansheng Huang和Ling Liu发表的论文介绍了POKELLMON&#xff0c;这是一个开创性的基于大模型&#xff08;LLM&#xff09;的具身智能体&#xff0c;它在战术战斗游戏中&#xff0c;特…

【Android 笔记】Android APK编译打包流程

前言 本文将介绍Android从一个项目打包成APK的过程&#xff0c;其中涉及Android Java和Kotlin文件、资源文件、清单文件、依赖jar包和so库等在打包过程中处理。 步骤 总体的打包流程如下图&#xff0c;下面就介绍下详细的打包步骤。 1、将aidl文件编译成java文件 在构建过程中…