第八章:Linux信号

这篇具有很好参考价值的文章主要介绍了第八章:Linux信号。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

系列文章目录



前言

linux信号是OS的重要功能。


linux中的信号

使用kill -l查看所有信号。使用信号时,可使用信号编号或它的宏。

1、Linux中信号共有61个,没有0、32、33号信号。

2、【1,31】号信号称为普通信号,【34,64】号信号称为实时信号。

[admin1@VM-4-17-centos linux_code]$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX
//查看系统定义的信号列表

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

以普通信号为例,进程task_struct结构体中存在unsigned int signal变量用以存放普通信号。(32个比特位中使用0/1存储、区分31个信号——位图结构)。

那么发送信号就是修改进程task_struct结构体中的信号位图。当然,有权限改动进程PCB的,也只有操作系统了。

进程对信号的处理

  1. 进程本身是程序员编写的属性和逻辑的集合;

  2. 信号可以随时产生(异步)。但是进程当前可能正在处理更为重要的事情,当信号到来时,进程不一定会马上处理这个信号;

  3. 所以进程自身必须要有对信号的保存能力;

  4. 进程在处理信号时(信号被捕捉),一般有三种动作:默认、自定义、忽略。

信号的释义

man 7 signal查看信号详细信息的命令

Trem:正常结束;Core:异常退出,可以使用核心转储功能定位错误,见本文第四节;Ign:内核级忽略。

挺多信号的功能都是一样的。这是因为不同的信号,可以代表发生了不同的事件,但处理结果可以一致。

信号的捕捉

信号的捕捉signal()

SIGNAL(2) 
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);//
signum:被捕捉的信号编号;
handler:对指定的信号设置自定义动作
handler设置为SIG_DFL表示信号默认处理方式,SIG_ING设置为忽略处理
#include <iostream>
#include <unistd.h>
#include <signal.h>
void hancler(int signo)
{
	//这里写自定义内容,捕获到signo信号后即可执行自定义代码
    std::cout<<"进程捕捉到信号"<<signo<<std::endl;
}
int main()
{
    signal(2,hancler);//外部需要对该进程发送信号
    while(1)
    {
        std::cout<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

外部需要对该进程发送信号,才能被signal接口捕捉。上面例子中,外部发送kill -2 PID或者键盘ctrl+c都行。

当捕捉到指定信号后,将会执行自定义函数。可用于信号功能的替换。

9号和19号信号无法被捕捉。kill -9乱杀进程,kill -19暂停进程。

信号的捕捉sigaction()

SIGACTION(2)    
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
signum:信号;
act:结构体对象;
oldact:输出型参数,记录原来的act对象
struct sigaction {
   void (*sa_handler)(int);//回调方法
   void (*sa_sigaction)(int, siginfo_t *, void *);
   sigset_t sa_mask;//阻塞信号集
   int sa_flags;
   void (*sa_restorer)(void);//用于支持旧版本的sigaction函数的信号处理函数地址,一般不使用。
};
Sigaction()在成功时返回0; 在错误时返回 -1,并设置 errno。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void hancler(int signo)
{
    std::cout<<"进程捕捉到信号"<<signo<<std::endl;
    sleep(10);
}
int main()
{
    //signal(2,hancler);//外部需要对该进程发送信号

    struct sigaction act, oact; //创捷结构体对象
    sigemptyset(&act.sa_mask);
    act.sa_handler = hancler;
    act.sa_flags = 0;
    sigaction(2, &act, &oact);

    while(1)
    {
    }
    return 0;
}

^C进程捕捉到信号2
^C^C^C^C^C^C^C^C进程捕捉到信号2
^C进程捕捉到信号2

当一个信号正在被递达执行期间,pending位图由1置0,同时该信号将被阻塞。

如果这时再接收到这个信号,发现该信号被阻塞,同时pending位图由0置1,保存这个信号。

若同一时间再接收到该信号,由于pending已存满,多余的该信号将被丢失。

当首个信号被捕捉完毕,操作系统会立即解除对该信号的屏蔽,因为pending位图对应的比特位是1,所以立即执行新的捕捉动作,同时pending位图该信号位由1清零。

这就是上图执行结果出现两次2号信号捕捉的原因。

信号的产生

第八章:Linux信号,# Linux,linux

通过终端按键产生信号

计算机是如何知道键盘输入了数据呢?键盘是通过硬件中断(由硬件完成)的方式,通知系统键盘输入了数据。

前台进程与后台进程

Linux只允许一个进程在前台,默认进程是bash,所以bash能够执行指令,可以用键盘按键来终止。

后台可以有多个进程运行,只能用 kill -9 来终止。

kill()用户调用kill向操作系统发送信号

KILL(2) 
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);//pid:目标进程的pid。sig:几号信号
成功时(至少发送了一个信号) ,返回零。出现错误时,返回 -1设置errno

通过命令行参数模仿写一个kill命令

//my_kill.cc
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <string>


void Usage(const std::string& proc)
{
    std::cout<< "Usage:" << proc << " pid " << "Signno\n" << std::endl; 
}

int main(int argc, char* argv[])//传参
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    pid_t pid = atoi(argv[1]);//获取第一个参数
    int signno = atoi(argv[2]);//获取第二个参数
    int n = kill(pid, signno);//需要发送信号的进程/发送几号信号
    if(n == -1)
    {
        perror("kill");
    }
    while(1)
    {
        std::cout << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

raise()进程自己给自己发任意信号(实际上是操作系统->进程)

RAISE(3)
#include <signal.h>
int raise(int sig);//sig:信号编号
raise()在成功时返回0,在失败时返回非0

raise(signo)等于kill(getpid,signo);

//当计数器运行到5时,进程会因3号进程退出
int main(int argc,char* argv[])//运行main函数时,需要先进行传参
{
    int cnt=0;
    while(cnt<=10)
    {
        std::cout<<cnt++<<std::endl;
        sleep(1);
        if(cnt>=5)
        {
            raise(3);
        }
    }
    return 0;
}

abort()进程自己给自己发6号信号

ABORT(3)
#include <stdlib.h>
void abort(void);
函数 abort()永远不会返回

abort()等于kill(getpid,SIGABRT);

硬件异常产生信号

硬件异常指非人为调用系统接口等行为,因软件问题造成的硬件发生异常。操作系统通过获知对应硬件的状态,即可向对应进程发送指定信号。

八号信号SIGFPE(除零错误可引发)

void hancler(int signno)
{
    std::cout << "进程捕捉到信号" << signno << std::endl;
}

int main()
{
    signal(8, hancler);

    int a = 1/0;

    while(1)
    {
        std::cout << "正在运行进程:" << getpid() << std::endl;

        sleep(1);
    }

    return 0;


}
进程捕捉到信号8
进程捕捉到信号8
进程捕捉到信号8
进程捕捉到信号8
进程捕捉到信号8^C

此时使用signal()捕捉这个信号,就会发现8号信号一直在被捕捉。这是因为状态寄存器是由CPU进行维护的,当8号信号被捕捉,进程并没有退出,根据时间片轮转,当进程被切换/剥离至CPU时,会读取和保存当前寄存器的上下文信息,所以我们就看到了8号信号被死循环捕捉。

十一号信号SIGSEGV(段错误可引发)

CPU中的硬件MMU通过页表拿到对应的物理地址,如果发现物理地址越界访问,操作系统就会向该进程发送11号信号。

软件条件产生异常

十三号信号SIGPIPE(匿名管道读端关闭,写端收到该信号)

例如匿名管道读端关闭,操作系统会向写端发送13号信号SIGPIPE终止写端。

十四号信号SIGALRM(定时器)

设置alarm函数是在告诉操作系统,将在设定的时间到来时,向进程发送14号信号终止进程。

ALARM(2)  
#include <unistd.h>
unsigned int alarm(unsigned int seconds);//seconds延时几秒
返回值为定时器剩余的秒数(可能会被提前唤醒)
alarm(0)表示取消之前设定的闹钟
int cnt = 0;
//设置一个cnt,用于测试代码在指定时间跑了多少
void hancler(int signo)
{
	//这里写自定义内容,捕获到signo信号后即可执行自定义代码
    std::cout<<"进程捕捉到信号"<<signo<<" "<<cnt<<std::endl;//检测到5秒后cnt为多少
    alarm(5);//循环捕捉闹钟
}
int main()
{
    signal(14,hancler);
    alarm(1);//定时1秒
    alarm(5);//定义新的闹钟,旧闹钟会失效哦
    while(1)
    {
        cnt++;
        sleep(1);
    }
    return 0;
}

闹钟是由软件实现的。任何一个进程,都可以通过alarm函数设定闹钟,所以操作系统需要通过先描述再组织的方式管理这些闹钟。

总结

  1. 所有信号产生,最终都要有操作系统来进行执行,因为操作系统是进程的管理者 。

  2. 信号如果没有被立即处理,那么信号将被保存至pending位图中。

  3. 一个进程在没有收到信号的时候,能否知道,自己应该对合法信号作何处理呢? 能,程序员写好了对应信号的处理方式(你没走人行道但你知道红灯停,绿灯行)。

  4. 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?都是借助OS向目标进程发送信号,即向目标进程pcb写信号位图。

  5. 信号产生方式:键盘/系统调用/指令/软件条件/硬件异常

进程退出时的核心转储

信号旁边写着Core的信号,都可以使用核心转储功能。

核心转储的定义

核心转储:当进程出现异常时,将进程在对应时刻的有效数据由内存存储至磁盘。

linux系统级别提供了一种能力,可以将一个进程在异常的时候,将内存中进程的相关数据,全部dump到磁盘中,一般会在当前进程的运行目录下,形成core.pid这样的二进制文件。

云服务器默认关闭了核心转储。在终端输入ulimit -a显示操作系统各项资源上限;使用ulimit -c 1000允许操作系统最大设置1000个block大小的数据块。

核心转储的意义

将程序异常的原因转储至磁盘,支持后续调试。

信号的保存(位图结构)

相关概念铺垫

  1. 信号递达(Delivery) :实际执行信号的处理动作。

  2. 信号未决(Pending):信号从产生到递达之间的状态。

  3. 进程可以选择阻塞 (Block )某个信号。

  4. 信号被阻塞时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

  5. 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

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

例如signal捕获信号的流程就是通过signo编号修改handler[signo]中的函数指针指向用户自定义的信号处理方法。当收到信号时,将pending位图中对应的比特位修改为1,若block位图中没有阻塞该信号,该信号被递达时就会执行该信号的处理方法。

对于普通信号,pending位图同时间只能保存一次同个信号,若该信号处于未递达状态,后续再次收到该信号将无法被保存(丢失)。

信号的处理(捕捉信号)

信号可以立即被处理吗?如果一个信号之前被block,当他解除block的时候,对应的信号会被立即递达。

为什么呢?信号的产生是异步的,当前进程可能正在做更重要的事情!什么时候是最合适的时间呢?当进程从内核态切换到用户态的时候,进程会在OS的指导下,进行信号的检测与处理!

再谈进程地址空间

用户态->内核态

进程如何从用户态切换至内核态并执行内核代码

每个进程的虚拟地址空间中有一块1G大小的内核空间,通过内核级页表映射的方式找到物理内存中内核代码进行执行。

由于内核级页表中对应物理地址的映射关系是一样的,所以每个进程都可以使用相同的内核级页表,无论进程如何切换,均可使用同一张内核级页表进行映射调用。

在进行用户态->内核态的切换过程中,首先通过CR3寄存器将进程状态由用户态修改为内核态(陷入内核),在本进程的内核空间中找到物理内存中的内核代码进行执行,执行完毕后将结果返回给进程。

信号的捕捉流程

信号的自定义捕捉:信号在产生的时候,不会被立刻处理,而是从内核态返回用户态的时候,对信号进行处理。

进程首先因为中断、异常、系统调用陷入内核,以内核态的身份运行内核代码,通过进程控制块中的信号位图分析当前信号的处理方式。

若为自定义处理,则需要进程回到用户态去执行用户设定的handler方法。为什么进程不能以内核态的身份直接执行handler方法?这是因为进程处于内核态,权限非常高,操作系统是没有能力识别代码的逻辑的,若handler被人为植入恶意代码,原先部分没有权限的代码因为执行身份的变化而被提权,所以操作系统必须让进程先回到用户态,降低进程的权限。

执行完handler方法后,进程需要重新回到内核态去执行一些系统调用,才能回退回用户态。

sigset_t信号集(调库,用于处理block和pending位图中的01)

每个信号只有一个bit的未决/阻塞标志,非0即1,不记录该信号产生了多少次。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集。这个类型可以表示每个信号的“有效”或“无效”状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

#include <signal.h>
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
int sigemptyset(sigset_t *set);
函数sigfifillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。 
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);

在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfifillset做初始化,使信号集处于确定的状态。初始化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参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。how:如何屏蔽信号集。

how 效果
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于mask=set

sigpending(获取当前进程的pending信号集)

SIGPENDING(2)
#include <signal.h>
int sigpending(sigset_t *set);//set:输出型参数,输出当前进程pending位图
sigending()在成功时返回0,在错误时返回-1。在发生错误时,将 errno 设置。

屏蔽信号并实时打印pending位图(运用上方三个接口)

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>

#define MAX_SIGNUM 31

static std::vector<int> signnoarr = {2};

static void show_pending(const sigset_t &pending)
{
    for (int signno = 1; signno < MAX_SIGNUM; ++signno)
    {
        if (sigismember(&pending, signno))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }

    std::cout << std::endl;
}

static void myhandler(int signno)
{
    std::cout << signno << "信号已被递达" << std::endl;
}

int main()
{
    for(const auto& signno : signnoarr) signal(signno, myhandler);
    //自定义递达

    //1.屏蔽指定信号
    sigset_t block, oblock, pending;

    //1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    //1.2 添加屏蔽信号
    for(const auto& signno : signnoarr) sigaddset(&block, signno);

    //1.3 开始屏蔽,设置进进程
    sigprocmask(SIG_SETMASK, &block, &oblock);


    //2.遍历打印pending信号集
    int cnt = 10;
    while(true)
    {
        //2.1 初始化
        sigemptyset(&pending);

        //2.2 获取进程信号集
        sigpending(&pending);

        //2.3 打印
        show_pending(pending);

        sleep(1);
        if(cnt-- == 0)
        {
            sigprocmask(SIG_SETMASK, &oblock, &block);//回复进程屏蔽字,OS至少递达一个信号
            std::cout << "恢复对信号的屏蔽,不屏蔽任何信号" << std::endl;
        }
    }

}
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000
^C010000000000000000000000000000
010000000000000000000000000000
010000000000000000000000000000
010000000000000000000000000000
010000000000000000000000000000
010000000000000000000000000000
010000000000000000000000000000
010000000000000000000000000000
2信号已被递达
恢复对信号的屏蔽,不屏蔽任何信号
000000000000000000000000000000
000000000000000000000000000000
000000000000000000000000000000

可重入函数

第八章:Linux信号,# Linux,linux

main函数调用insert函数向一个链表head中插入节点P1,插入操作分为两步,刚执行完第一句代码,此时硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作执行完毕后,sighandler返回内核态,再次回到用户态就从main函数继续执行刚才剩余的代码。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有P1真正插入链表中,P2这个节点谁都找不到了。发生内存泄漏。

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱。像这样的函数称为不可重入函数,反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

不可重入函数:调用了malloc或free,因为malloc也是用全局链表来管理堆的。 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

volatile关键字

优化后,通过信号自定义方法handler修改全局q,但是程序不会退出。

O3优化时:编译器认为q在main执行流中没有被修改,所以编译器对q做了优化,直接将q放在了寄存器中,这样后续执行时就不用再去内存中读取q了,提高了程序运行效率。虽然handler中修改了内存中的q,但是寄存器中的q值一直是1(寄存器中的q值是临时值,操作系统没有对其进行修改),所以会发生上图效果。

解决方法:给q加volatile关键字,让q通过内存读取而不是寄存器,保持变量q的内存可见性。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
volatile int q=1;//保持内存可见性
void handler(int signo)
{
    q=0;
}
int main()
{
    signal(2,handler);
    while(q!=0);
    return 0;
}

SIGCHLD信号

1、子进程退出,会向父进程发送17号信号SIGCHLD;

2、由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。

//忽略子进程发出的17号信号
signal(SIGCHLD,SIG_IGN);
sigaction(SIGCHLD,act,oldact);//act中忽略17号信号

系统默认的忽略动作和用户用signal/sigaction函数自定义的忽略 通常是没有区别的,但这里是一个特例。

虽然信号SIGCHID的默认动作也是忽略,但这个忽略是实实在在的无视了这个信号;我们手动在handler方法中使用SIG_IGN,子进程退出时发送给父进程的信号将会被父进程忽略,但子进程会被操作系统回收,这就是区别所在。文章来源地址https://www.toymoban.com/news/detail-635936.html

到了这里,关于第八章:Linux信号的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 第八章,帖子列表

     

    2024年02月11日
    浏览(37)
  • 第八章:多线程

    目录 八:多线程 8.1:基本概念 8.2:线程的创建与使用         8.2.1:Thread类的有关方法         8.2.2:线程的调度         8.2.3:两种创建线程方式的比较 8.3:线程的生命周期 8.4:线程的同步         8.4.1:同步代码块同步方法         8.4.2:单例模式的懒汉式修改

    2024年02月09日
    浏览(35)
  • C国演义 [第八章]

    力扣链接 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格 你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润 返回你可以从这笔交易中获取的最大利润。如果你不能获

    2024年02月15日
    浏览(44)
  • 第八章 C#脚本(上)

    脚本是使用 Unity 开发的所有应用程序中必不可少的组成部分。大多数应用程序都需要脚本来响应玩家的输入并安排游戏过程中应发生的事件。游戏对象的行为由附加的组件控制。虽然Unity内置了许多组件,但是我们仍然可以使用脚本来创建自定义组件。Unity支持C#编程脚本语言

    2024年02月01日
    浏览(96)
  • 第八章 函数探幽

    提出的目的 :为了提高程序运行速度。 内联函数和普通函数的区别: 编译方式 : 内联函数在编译时会被直接替换到调用处,而不是像普通函数那样通过函数调用的方式执行。这样可以减少函数调用的开销,提高程序执行效率。 普通函数则是通过函数调用的方式执行,会涉

    2024年03月13日
    浏览(39)
  • 第八章 贪心

    Leetcode 455 思路一:大饼干喂给大胃口 上面的代码一定要是 for 控制胃口,if 控制饼干,因为 for 中的 i 使固定移动的! 思路二:小饼干喂给小胃口 Leetcode 1005 Leetcode 860 三种情况: 情况一:账单是5,直接收下。 情况二:账单是10,消耗一个5,增加一个10 情况三: 账单是20,

    2024年02月06日
    浏览(47)
  • 考研C语言第八章

    这个东西看着像数据库里面属性的定义,也像java里面的类的定义 关于结构体里面scanf读取输入的数据,并进行相关的存储, 这里面字符串就像之前的,取数据时可以和前面的不加空格,可以不加取地址符号 (但是为了好记和规范,建议直接所有的一视同仁) 就想数据库在输

    2024年02月10日
    浏览(36)
  • 考研数据结构:第八章 排序

    2.1.1算法思想 插入排序的思想很简单,就是不断的把一个个带排序的记录,按的大小插入到前面已经排好序的子序列中。直到全部序列插入完成。 比如我们现在要对下面的序列进行排序, 刚开始我们从1号位开始 我们会认为当前处理的这个元素之前都是有序的 现在把

    2024年02月11日
    浏览(37)
  • ChatGPT入门到高级【第八章】

    第一章:Chatgpt的起源和发展 1.1 人工智能和Chatbot的概念 1.2 Chatbot的历史发展 1.3 机器学习技术在Chatbot中的应用 1.4 Chatgpt的诞生和发展 第二章:Chatgpt的技术原理 2.1 自然语言处理技术 2.2 深度学习技术 2.3 Transformer模型 2.4 GPT模型 第三章:Chatgpt的应用场景 3.1 智能客服 3.2 智能问

    2024年02月05日
    浏览(36)
  • Python第八章作业(初级)

    任务描述 读取附件是一篇英文短文,请编写程序统计这篇短文前 n 行中每一个英文字母出现的次数,结果按次数降序排列,次数相同时,按字母表顺序输出。若 n 值大于短文行数,输出整篇文章中每一个英文字母出现的次数(大写字母按小写字母统计)。 输入格式 输入一个

    2024年02月06日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包