Linux信号

目录

信号入门

1. 生活角度的信号

2. 技术应用角度的信号

3. 注意

4. 信号概念

5. 用kill -l命令可以察看系统定义的信号列表

 6. 信号处理常见方式概览

产生信号

1. 通过终端按键产生信号

2. 调用系统函数向进程发信号

3. 由软件条件产生信号

4. 硬件异常产生信号

核心转储(调试技巧)

总结

阻塞信号

1. 信号其他相关常见概念

2. 在内核中的表示

3. sigset_t

4. 信号集操作函数

sigprocmask

sigpending

捕捉信号

2. sigaction

可重入函数,以及volatile

SIGCHLD信号 - 了解


信号入门

1. 生活角度的信号

当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需 5min 之后才能去取快递。那么在在这5min 之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“ 在合适的时候去取“
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
下面我们回答几个问题:
进程是怎么识别信号的?  认识+动作
当进程接受到信号的时候,不一定立刻进行处理,有可能先存储起来,稍后处理
进程必须要有保存信号的能力
如果信号要保存,那么应该保存在哪里呢?学习过之前的知识,我们可以推测到可能保存到了PCB中了,因为OS要管理进程,就必须要先描述,再组织。而其实 发送信号的本质其实是修改PCB中的位图而已,我们可以通过 kill -l来查看信号

 如果是31个信号的话,我们就可以使用位图来表示,每个比特位表示一个信号状态。这样我们通过修改位图的方式就可以发送信号了。但是想要修改位图,我们就必须要改变通过OS,但是OS又不信任我们用户,所以就会提供系统调用接口。这样就完全和前面的知识都串联起来了。

2. 技术应用角度的信号

1. 用户输入命令,Shell下启动一个前台进程。

用户按下 Ctrl-C , 这个键盘输入 产生一个硬件中断 ,被 OS 获取,解释成信号,发送给目标前台进程
前台进程因为收到信号,进而引起进程退出
这里的Ctrl - C等于2号信号,我们可以通过改变2号的方式,让Ctrl-C不再能退出
代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;

void helper(int signo)
{
    cout<<"捕捉到一个信号,其信息编号是"<<signo<<endl;
}

int main()
{
    signal(2,helper);
    while(true)
    {
        cout<<"我是一个进程,我的id是:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

结果:我们没有办法使用ctrl -c来结束

3. 注意

1. Ctrl - C 产生的信号只能发给前台进程。一个命令后面加个 & 可以放到后台运行 , 这样 Shell 不必等待进程结束就可以接受新的命令, 启动新的进程。
2. Shell 可以同时运行一个前台进程和任意多个后台进程 , 只有 前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
3. 前台进程在运行过程中用户随时可能按下 Ctrl - C 而产生一个信号 , 也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止 , 所以 信号相对于进程的控制流程来说是异步
(Asynchronous) 的。

4. 信号概念

信号是进程之间事件异步通知的一种方式,属于软中断

5. kill -l命令可以察看系统定义的信号列表

 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2

这些信号各自在什么条件下产生, 默认的处理动作是什么 , signal(7) 中都有详细说明 : man 7 signal

 6. 信号处理常见方式概览

处理动作有以下三种 :
1. 忽略此信号。
2. 执行该信号的默认处理动作。
3. 提供一个信号处理函数 , 要求内核在处理该信号时切换到用户态执行这个处理函数 , 这种方式称为捕捉(Catch)一个信号。

产生信号

1. 通过终端按键产生信号

前面我们已经通过ctrl + c的方式终止了进程,这就是其中的一个方法,当然还有其他的快捷点替代其他的信号。

2. 调用系统函数向进程发信号

我们先看看系统调用的3个函数可以对进程发信号:
1.2的头文件都是:  #include <signal.h>
1.kill :可以想任意进程发送任意信号
int kill(pid_t pid, int signo);
2.raise:给自己 发送 任意信号    等价于   kill(getpid(), 任意信号)
int raise(int signo);
3.abort : 给自己 发送 指定的信号SIGABRT 等价于  kill(getpid(), SIGABRT)
#include <stdlib.h>
void abort(void);
我们可以看到其实kill的系统调用函数很强大,另外两个都可以通过kill来模拟实现。
第一个系统函数:kill
我们可以通过一个函数让一个进程去给另一个发信号:
#include <iostream>
using namespace std;
#include <string>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <signal.h>

void Usage(const string& proc)
{
    cout<<"Usage: "<<"pid  signo"<<endl;
    exit(1);
}
int main(int argc,char*argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
    }
    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);
    int n = kill(pid,signo);
    assert(n == 0);
    return 0;
}
 结果:
第二个系统函数raise:
int main(int argc,char*argv[])
{
    int cnt = 0;
    while(cnt < 10)
    {
        cout<<"我是一个进程,我的id以及次数cnt是:"<<getpid()<<" "<<cnt++<<endl;
        if(cnt == 5)
        {
            raise(9);
        }
        sleep(1);
    }
}
第三个系统函数:abort
int main(int argc,char*argv[])
{
    int cnt = 0;
    while(cnt < 10)
    {
        cout<<"我是一个进程,我的id以及次数cnt是:"<<getpid()<<" "<<cnt++<<endl;
        if(cnt == 5)
        {
            //raise(9);
            abort();
        }
        sleep(1);
    }
}

结果和raise的是一样的

3. 由软件条件产生信号

这里主要介绍 alarm函数 和SIGALRM信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用 alarm 函数可以设定一个闹钟 , 也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号 , 该信号的默认处理动作是终止当前进程。
int count = 1;
void catchsig(int signo)
{
    cout<<"捕捉到一个信号 signo:   "<<signo<<"count:"<<count<<endl;
    exit(1);
}

int main(int argc,char*argv[])
{
    signal(SIGALRM,catchsig);
    alarm(1);
    while(true)
    {
        //cout<< "我是一个进程,正在打印count的值"<<count++<<endl;
        count++;
    }
}

看看结果:

通过对比这两次的结果我们不难发现,cpu的运行速度远超打印的速度。这里也体现的alarm的使用

可能很多进程都需要使用的闹钟,所以闹钟需要被OS系统进行管理,如何管理?先组织再描述。使用struct结构体去封装,然后通过特定的数据结构进行管理,那么操作系统对闹钟的管理其实就变成了对数据结构的增删查改。

4. 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。 例如当前进程执行了除以0 的指令 ,CPU 的运算单元会产生异常 , 内核将这个异常解释 为 SIGFPE 信号发送给进程。再比如当前进程访问了非法内存地址,,MMU 会产生异常 , 内核将这个异常解释为 SIGSEGV 信号发送给进程。

我们先通过上面的例子来验证一下:

 然后我们看看这个错误是哪个信号造成的:

然后我们再通过代码来验证一下:

void catchsig(int signo)
{
    cout<<"捕捉到一个信号 signo:   "<<signo<<"count:"<<count<<endl;
    exit(1);
}
int main(int argc,char*argv[])
{
    signal(SIGFPE,catchsig);
    int a = 10;
    a /= 0;
}

 看看结果:

 我们可以通过画图来理解一下:

 如果是空指针的解引用呢?

 画图理解:
在之前学习进程地址空间的时候,我们就知道操作系统不信任何人,所以我们写的代码如果有错误是不会在真实的物理内存上运行的。

核心转储(调试技巧)

 假设我们写了一个数组越界的代码:

如果我们正常运行是有可能报错,也有可能不报错的,因为对于C语言的数组的检测方式是抽查,而不像C++vector的暴力检查。

如果出现段错误,那么我们基本可以肯定的是我们的代码可能是越界访问了。

 我们可以看到这个文件的大小为0,我们可以通过下面指令来扩大其size

 然后我们再运行一下刚刚那个错误代码:

 当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据转储到磁盘中,这就是核心转储,那为什么要核心转储呢?是为了方便我们调试,怎么支持调试?我们可以在gdb的上下文中输入code-file core.xxxx

 在看过之前我们使用signal进行了信号行为的替换之后,我们可能有这样的想法:如果把每个信号都替换了,那这个进程不就杀不死了吗?真的如此吗?我们可以试试:

void catchsig(int signo)
{
    cout<<"捕捉到一个信号 signo:   "<<signo<<endl;
    //exit(1);
}
int main(int argc,char*argv[])
{
    for(int signo = 1;signo<=31;++signo)
    {
        signal(signo,catchsig);
    }
    while(true)
    {
        cout<<"hello world"<<endl;
        sleep(1);
    }
}

看看结果:

从这里我们可以看出来,操作系统是不允许出现杀不死进程这种情况的,我们可以通过kill -9 杀死。 

我们看看另外一个问题:看看这段代码:

void catchsig(int signo)
{
    cout<<"捕捉到一个信号 signo:   "<<signo<<endl;
    //exit(1);
}
int main()
{
  signal(8,catchsig);
    while(1)
    {
        cout<<"我是一个进程,正在运行 "<<getpid()<<endl;
        sleep(1);
        int a = 10;
        a/=0;
    }
    return 0;
}

 我们会发现这里一直打印我们替换了函数的内容,为什么呢?

因为我们收到信号并没有退出,而CPU之后一个,每次进程使用后cpu的时候,都被cpu的寄存器检测到了那个异常的比特位,所以就会进行打印。而进程并没有退出,所以就会有无数次进程的状态被寄存器保存和恢复。

总结

1.上面所说的所有信号产生,最终都要有 OS 来进行执行,为什么? OS 是进程的管理者
2.信号的处理是否是立即处理的?在合适的时候,可能进程在更重要的事情。
3.信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
需要被进程暂时记录下来,记录在PCB中最合适
4.一个进程在没有收到信号的时候,能否知道,自己应该对合法信号作何处理呢?
肯定知道,因为只有知道了,接受后才会采取相应的措施
5.如何理解 OS 向进程发送信号?能否描述一下完整的发送处理过程?
OS向进程发送信号的本质其实就是在进程的PCB中的位图中的比特位由0置为1

阻塞信号

1. 信号其他相关常见概念

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

2. 在内核中的表示

因为信号的发送其实本质是在进程的PCB中把其中的位图由0置为1。

那么我们常用的信号有31种,我们只需要一个int大小的位图就可以实现对信号的保存。

 这样我们就可以通过改变pending或者block位图中的bit就可以对信号进行接受或者屏蔽。

 通过大致上面的代码我们就可以大致了解了信号是怎么被处理的

3. sigset_t

未决和阻塞标志可以用相同的数据类型sigset_t来存储 ,sigset_t 称为信号集 , 这个类型可以表示每个信号的“ 有效 无效 状态 , 在阻塞信号集中 有效 无效 的含义是该信号是否被阻塞 , 而在未决信号集中 有效” 无效 的含义是该信号是否处于未决状态

4. 信号集操作函数

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

sigprocmask

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值 : 若成功则为 0, 若出错则为 -1
如果 oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出 。如果 set是非空指针,则 更改进程的信号屏蔽字 , 参数 how 指示如何更改。 如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里 , 然后根据set how 参数更改信号屏蔽字。

how参数:

 SIG_BLOCK是屏蔽信号使用的,SIG_UBLOCK是解除屏蔽的信号,SIG_SETMASK是将当前的set改成你自己想要的set;

sigpending

读取当前进程的未决信号集 , 通过 set 参数传出。调用成功则返回 0, 出错则返回 -1

我们可以利用上述学的函数可以写个小代码:

思路:我们可以在进程运行的时候查看信号的状态,我们可以在前10s内将某个或者多个信号屏蔽,然后通过使用发送信号,然后查看信号是否被屏蔽

代码实现:

#include <iostream>
#include <vector>
#include <unistd.h>
#include <signal.h>
using namespace std;
#define MAX_SIG 31 // 信号的个数
// 创建一个全局的数组,这里可以屏蔽多个信号
static vector<int> sig_arr = {2};

void printsig(const sigset_t &set)
{
    for (int i = MAX_SIG; i > 0; --i)
    {
        // 判断第几号信号是否在pending位图中
        if (sigismember(&set, i))
        {
            cout << "1";
        }
        else cout << "0";
    }
    cout << endl;
}

void myhander(int signo)
{
    cout << signo << "号信号已经执行" << endl;
}

int main()
{
    // 我们可以把信号执行的方法改一下,方便更好的观察
    for (const auto &sig : sig_arr)
    {
        signal(sig, myhander);
    }
    // 设置屏蔽的指定信号
    sigset_t block, oblock, pending;
    // 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);
    // 添加屏蔽的信号
    for (const auto &sig : sig_arr)
    {
        sigaddset(&block, sig);
    }
    // 把屏蔽的信号设置进内核
    sigprocmask(SIG_SETMASK, &block, &oblock);
    // 然后通过观察pending位图中的01序列即可知道是否完成屏蔽
    // 这里10s后就取消屏蔽
    int cnt = 10;
    while (true)
    {
        // 初始化,这里其实没有必要
        sigemptyset(&pending);
        // 获取
        sigpending(&pending);
        // 自己写一个打印函数去观察
        printsig(pending);
        //1s打印一次
        sleep(1);
        if (cnt-- == 0)
        {
            // 取消屏蔽然后再观察,因为前面已经保存了之前的状态,这里反过来重新设置即可
            sigprocmask(SIG_SETMASK, &oblock, &block);
            // 这里屏蔽信号之后会立刻处理至少一个的信号
            cout << "取消屏蔽信号" << endl;
        }
    }
    return 0;
}

看看结果:

 因为当我们取消屏蔽这个信号之后,OS至少要处理一个信号,如果这里我们没有把信号处理的默认方法改了,那么我们就无法看到取消屏蔽信号这句话了。

捕捉信号

我们通过上面这段代码应该有这样的疑问:我们在使用系统调用接口的时候,身份应该要改变才能执行系统的代码才对,那么我们是怎么样由用户态转换成内核态的呢?

这里我们的CPU就通过一个寄存器来实现内核态和用户态身份的转换,有一个CR3的寄存器来存储当前进程的运行级别,0表示内核态,3表示用户态,我们在执行系统内部的代码的时候OS是要看进程的身份才能执行的。因为OS不相信任何人。如果我们大量使用系统调用那么必然导致的一个问题就是效率大大降低,因为身份转换是需要时间的,所以为了保证效率,我们要尽可能的少点使用系统接口

那么还有一个问题:既然是一个进程在执行,怎么会跑到OS去执行了呢?怎么做到的?

在之前我们学习进程地址空间的时候其实还有一部分内容没有完全学到:

 通过上图我们就可以知道实际上我们的进程地址空间其实有1G就内核级的。

那么刚刚的代码的信号捕捉的过程就怎么样的呢?

我们可以通过上图来理解,首先我们在用户态,然后执行系统调用我们需要进行一次身份切换,然后找PCB中的block位图已经pending位图,如果用户有对应的方法,我们就需要 回答用户态执行对应的方法,那么有个问题:此时我们是否需要进行身份切换呢?答案是一定要的,因为如果我们以内核态来执行对应的代码就有可能对OS的内部信息做修改,这是OS不允许发生的事情,所以这里必然也有一次身份切换,执行完用户态代码,我们就回到了内核态,然后通过特定的系统调用返回用户态继续执行接下来的代码。所以其中总共有4次的身份切换。

2. sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction 函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回 0, 出错则返回 - 1 signo是指定信号的编号。若act 指针非空 , 则根据 act 修改该信号的处理动作。若 oact 指针非 空 , 则通过 oact 传出该信号原来的处理动作。act oact 指向 sigaction 结构体 :

这个结构体中比较重要的的就是这个sa_mask,通过这个可以设置其他的信号也会被屏蔽

当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就 保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止 , 信号处理函数返回时自动恢复原来的信号屏蔽字

 总之就是在一个信号接受的时候会自动屏蔽同类信号,知道该信号处理完毕才会恢复成原来的情况,如果这里我们会2号信号进行多次的发送,这里只会有2次2号信号的处理:

void Count(int n)
{
    //用来计数
    while(n)
    {
        printf("count: %2d\r",n);
        fflush(stdout);
        --n;
        sleep(1);
    }
    cout<<endl;
}
void handler(int signo)
{
    cout<<"接受到一个信号,signo:"<<signo<<endl;
    Count(10);
}
int main()
{
    struct sigaction act,oact;
    act.sa_flags = 0;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    //我们也可以把其他信号屏蔽放进来
    sigaddset(&act.sa_mask,3);
    sigaction(2,&act,&oact);
    while(1)
    {
        //cout<<"mypid :"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

我们可以看看结果:

 为什么是这样的结果呢?

很简单,首先这里2号信号被屏蔽了,当我们再次发送2号信号的时候只能改一次pending位图,所以只能接受一次信号,而我们上面是已经屏蔽了3号信号的,所以,当2号信号执行完成之后,就会处理3号信号,进程就会退出。

可重入函数,以及volatile

如果一个函数符合以下条件之一则是不可重入的 :
调用了 malloc free, 因为 malloc 也是用全局链表来管理堆的。
调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。
以上两个条件决定了大部分的函数都是不可重入的函数,我们可以看看下面这个例子,来了解这个概念:
int n = 0;

void handler(int signo)
{
     cout<<n<<endl;
     ++n;
     cout<<n<<endl;
}
int main()
{
    
    signal(2,handler);
    while(!n)
    {
        //这里什么都不做,如果没有退出就说明有问题
    }
    cout<<"我是一个正常退出的进程"<<endl;
    return 0;
}

这是正常情况下的:

 当我们把编译器优化开到O3的时候,这个问题就体现出来了:

但是如果我们使用了C语言的volatile关键字就不会出现这个问题:

 

 

 这个关键字的作用就是保证内存的可见性,那么上述问题为什么会这样呢,我们画图分析:

 volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

SIGCHLD信号 - 了解

进程一章讲过用 wait waitpid 函数清理僵尸进程 , 父进程可以阻塞等待子进程结束 , 也可以非阻 塞地查询是否有子进程结束等待清理( 也就是轮询的方式 ) 。采用第一种方式 , 父进程阻塞了就不 能处理自己的工作了 ; 采用第二种方式 , 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号 , 该信号的默认处理动作是忽略 , 父进程可以自 定义 SIGCHLD 信号的处理函数, 这样父进程只需专心处理自己的工作 , 不必关心子进程了 , 子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可
由于 UNIX 的历史原因 , 要想不产生僵尸进程还有另外一种办法 : 父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程 。系统默认的忽略动作和用户用sigaction 函数自定义的忽略 通常是没有区别的 , 但这是一个特例。 此方法对于Linux可用,但不保证在其它UNIX系统上都可 用 。OS会自动回收子进程。
void Count(int n)
{
    //用来计数
    while(n)
    {
        printf("count: %2d\r",n);
        fflush(stdout);
        --n;
        sleep(1);
    }
    cout<<endl;
}
int main()
{
    //显示设置,这样OS可以自动的回收子进程
    signal(SIGCHLD,SIG_IGN);
    cout<<"我是父进程,我的ID是:"<<getpid()<<endl;
    int n = fork();
    if(n == 0)
    {
        printf("我是一个子进程,我的id是 %d,父进程id是: %d\n",getpid(),getppid());
        Count(10);
        exit(1);
    }
    //父进程什么都不做,不需要回收子进程,
    while(true)
        sleep(1);
    return 0;
}

 

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

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

相关文章

DHCP原理简析及交互实践

环境&#xff1a; os&#xff1a;centos7 dnsmasq&#xff1a;version 2.76 一. dhcp工作原理 首先补充几个dhcp相关的基本概念&#xff1a; 1、动态主机配置协议DHCP&#xff08;Dynamic Host Configuration Protocol&#xff09;是一种网络管理协议&#xff0c;用于集中对用…

程序员必会技能—— 使用日志

目录 1、为什么要使用日志 2、自定义日志打印 2.1、在程序中得到日志对象 2.2、使用日志对象打印日志 2.3、日志格式 3、日志的级别 3.1、日志级别的分类 3.2、日志级别的设置 4、持久化日志 5、更简单的日志输出——lombok 5.1、如何在已经创建好的SpringBoot项目中添加…

Python+ChatGPT实战之进行游戏运营数据分析

文章目录一、数据二、目标三、解决方案1. DAU2. 用户等级分布3. 付费率4. 收入情况5. 付费用户的ARPU最近ChatGPT蛮火的&#xff0c;今天试着让ta写了一篇数据分析实战案例&#xff0c;大家来评价一下&#xff01;一、数据 您的团队已经为您提供了一些游戏数据&#xff0c;包括…

MySQL数据库的基础语法总结(1)

MySql一.数据库,数据表的基本操作1.数据库的基本操作2. 数据表的基本操作2.1 数据库的数据类型2.1.1 整数类型2.1.2 浮点数类型和定点数类型2.1.3 字符串类型2.1.4 日期与时间类型2.2 数据表的基本操作2.2.1 创建一个数据表2.2.2 查看数据表2.2.3 查看表的基本信息的MySQL指令2…

【拳打蓝桥杯】最基础的数组你真的掌握了吗?

文章目录一&#xff1a;数组理论基础二&#xff1a;数组这种数据结构的优点和缺点是什么&#xff1f;三&#xff1a;数组是如何实现随机访问的呢&#xff1f;四&#xff1a;低效的“插入”和“删除”原因在哪里&#xff1f;五&#xff1a;实战解题1. 移除元素暴力解法双指针法2…

前端开发神器VS Code安装教程

✅作者简介&#xff1a;CSDN一位小博主&#xff0c;正在学习前端 &#x1f4c3;个人主页&#xff1a;白月光777的CSDN博客 &#x1f4ac;个人格言&#xff1a;但行好事&#xff0c;莫问前程 安装VS CodeVS Code简介VS Code安装VS Code汉化结束语&#x1f4a1;&#x1f4a1;&…

嵌入式学习笔记——STM32的USART收发字符串及串口中断

USART收发字符串及串口中断前言字符串的收发发送一个字符串接收字符串需求利用串口实现printf中断中断是什么前言 上一篇中&#xff0c;介绍了串口收发相关的寄存器&#xff0c;通过代码实现了一个字节的收发&#xff0c;本文接着上面的内容&#xff0c;通过功能函数实现字符串…

Elasticsearch:集群管理

在今天的文章中&#xff0c;我们应该学习如何管理我们的集群。 备份和分片分配是我们应该能够执行的基本任务。 分片分配过滤 Elasticsearch 将索引配到一个或多个分片中&#xff0c;我们可以将这些分片保存在特定的集群节点中。 例如&#xff0c;假设你有多个数据集群节点&am…

项目实战-瑞吉外卖day02(B站)持续更新

瑞吉外卖-Day02课程内容完善登录功能新增员工员工信息分页查询启用/禁用员工账号编辑员工信息分析前端页面效果是如何实现的为什么点击左边 右边会根着变化首先 我们先来看一下菜单是如何展示出来的 在来看一下 为啥点击菜单时 右边会跟着变第一 &#xff1a;菜单是如何展示出来…

【剑指offer-C++】JZ32:从上往下打印二叉树

题目描述 描述&#xff1a;不分行从上往下打印出二叉树的每个节点&#xff0c;同层节点从左至右打印。例如输入{8,6,10,#,#,2,1}&#xff0c;如以下图中的示例二叉树&#xff0c;则依次打印8,6,10,2,1(空节点不打印&#xff0c;跳过)&#xff0c;请你将打印的结果存放到一个数…

我用Python写了一个下载网站所有内容的软件,可见即可下,室友表示非常好用

Python 写一个下载网站内容的GUI工具&#xff0c;所有内容都能下载&#xff0c;真的太方便了&#xff01;前言本次要实现的功能效果展示代码实战获取数据GUI部分最后前言 哈喽大家好&#xff0c;我是轻松。 今天我们分享一个用Python写下载视频弹幕评论的代码。 之前自游写了…

计算机网络复习

什么是DHCP和DNS DNS(Domain Name System&#xff0c;域名系统)&#xff0c;因特网上作为域名和IP地址相互映射的一个分布式数据库&#xff0c;能够使用户更方便的访问互联网&#xff0c;而不用去记住能够被机器直接读取的IP数串。通过主机名&#xff0c;最终得到该主机名对应的…

ARM uboot 的移植4 -从 uboot 官方标准uboot开始移植

一、添加DDR初始化1 1、分析下一步的移植路线 (1) cpu_init_crit 函数成功初始化串口、时钟后&#xff0c;转入 _main 函数&#xff0c;函数在 arch/arm/lib/crt0.S 文件中。 (2) 在 crt0.S 中首先设置栈&#xff0c;将 sp 指向 DDR 中的栈地址&#xff1b; #if defined(CONF…

被骗进一个很隐蔽的外包公司,入职一个月才发现,已经有了社保记录,简历污了,以后面试有影响吗?...

职场的套路防不胜防&#xff0c;一不留神就会掉坑&#xff0c;一位网友就被“骗”进了外包公司&#xff0c;他说公司非常隐蔽&#xff0c;入职一个月才发现是外包&#xff0c;但已经有了社保记录&#xff0c;简历污了&#xff0c;不知道对以后面试有影响吗&#xff1f;楼主说&a…

【含源码】用python做游戏有多简单好玩

有很多同学问我还有其他什么小游戏吗&#xff0c;游戏是怎么做的&#xff0c;难不难。我就用两篇文章来介绍一下&#xff0c;如何使用Python做游戏。 兔子与灌 俄罗斯方块 休闲五子棋 走迷宫 推箱子 消消乐 超多小游戏玩转不停↓ 更多小游戏可以评论区讨论哦&#xff0c;喜欢…

保姆级使用PyTorch训练与评估自己的Replknet网络教程

文章目录前言0. 环境搭建&快速开始1. 数据集制作1.1 标签文件制作1.2 数据集划分1.3 数据集信息文件制作2. 修改参数文件3. 训练4. 评估5. 其他教程前言 项目地址&#xff1a;https://github.com/Fafa-DL/Awesome-Backbones 操作教程&#xff1a;https://www.bilibili.co…

【C++】模板进阶

模板进阶 文章目录模板进阶1、非类型模板参数2、模板的特化2.1.概念2.2.函数模板特化2.3.类模板特化2.3.1.全特化2.3.2.偏特化2.4.类模板特化示例3、模板总结1、非类型模板参数 模板参数分类类型形参与非类型形参。类型模板参数&#xff1a;出现在模板参数列表中&#xff0c;跟…

技术人的管理学-团队管理

主要内容前言团队管理&#xff0c;如何让猪上树&#xff1f;选人育人用人留人总结前言 本周报名参加了 风 变 科 技推出的管理学MTP培训课&#xff0c;花了点小钱&#xff0c;感觉还是有用的&#xff0c;可惜时间安排都在8点以后&#xff0c;刚好是看孩子的时间&#xff0c;只…

io流概述

public static void main(String[] args) throws IOException {//1.与文件建立联系 File->数据源File src new File("D://AAA/test.txt");//2.创建文件字节输入流->管道//InputStream is new FileInputStream(src);InputStream is new FileInputStream(&quo…

中科亿海微FPGA

国产FPGA中&#xff0c;紫光、安路、高云称得上是三小龙&#xff0c;其他的半斤八两&#xff0c;中科亿海微也算是其中之一。 其产品为亿海神针系列&#xff0c;如下&#xff1a; 可见其最小规模也有9.2KLUT&#xff0c;最大竟有136K之多了&#xff0c;对比其他国产&#xff0…
最新文章