Linux信号机制-2

转自:Linux信号处理_linux 信号处理函数_努力啃C语言的小李的博客-CSDN博客

什么是信号

信号本质上是在软件层次上对中断机制的一种模拟,其主要有以下几种来源:

    程序错误:除零,非法内存访问等。

    外部信号:终端 Ctrl-C 产生 SGINT 信号,定时器到期产生SIGALRM等。

    显式请求:kill函数允许进程发送任何信号给其他进程或进程组。

目前 Linux 支持64种信号。信号分为非实时信号(不可靠信号)和实时信号(可靠信号)两种类型,对应于 Linux 的信号值为 1-31 和 34-64。

信号是异步的,一个进程不必通过任何操作来等待信号的到达。事实上,进程也不知道信号到底什么时候到达。一般来说,我们只需要在进程中设置信号相应的处理函数,当有信号到达的时候,由系统异步触发相应的处理函数即可。如下代码:

    #include <signal.h>
    #include <unistd.h>
    #include <stdio.h>
     
    void sigcb(int signo) {
        switch (signo) {
        case SIGHUP:
            printf("Get a signal -- SIGHUP\n");
            break;
        case SIGINT:
            printf("Get a signal -- SIGINT\n");
            break;
        case SIGQUIT:
            printf("Get a signal -- SIGQUIT\n");
            break;
        }
        return;
    }
     
    int main() {
        signal(SIGHUP, sigcb);
        signal(SIGINT, sigcb);
        signal(SIGQUIT, sigcb);
        for (;;) {
            sleep(1);
        }
    }

运行程序后,当我们按下 Ctrl+C 后,屏幕上将会打印 Get a signal -- SIGINT。当然我们可以使用 kill -s SIGINT pid命令来发送一个信号给进程,屏幕同样打印出 Get a signal -- SIGINT 的信息。

信号实现原理

接下来我们分析一下Linux对信号处理机制的实现原理。

在进程管理结构 task_struct 中有几个与信号处理相关的字段,如下:

    struct task_struct {
        ...
        int sigpending;
        ...
        struct signal_struct *sig;
        sigset_t blocked;
        struct sigpending pending;
        ...
    }

 成员 sigpending 表示进程是否有信号需要处理(1表示有,0表示没有)。成员 blocked 表示被屏蔽的信息,每个位代表一个被屏蔽的信号。成员 sig 表示信号相应的处理方法,其类型是 struct signal_struct,定义如下:

    #define  _NSIG  64
     
    struct signal_struct {
        atomic_t        count;
        struct k_sigaction    action[_NSIG];
        spinlock_t        siglock;
    };
     
    typedef void (*__sighandler_t)(int);
     
    struct sigaction {
        __sighandler_t sa_handler;
        unsigned long sa_flags;
        void (*sa_restorer)(void);
        sigset_t sa_mask;
    };
     
    struct k_sigaction {
        struct sigaction sa;
    };

可以看出,struct signal_struct 是个比较复杂的结构,其 action 成员是个 struct k_sigaction 结构的数组,数组中的每个成员代表着相应信号的处理信息,而 struct k_sigaction 结构其实是 struct sigaction 的简单封装。

我们再来看看 struct sigaction 这个结构,其中 sa_handler 成员是类型为 __sighandler_t 的函数指针,代表着信号处理的方法。

最后我们来看看 struct task_struct 结构的 pending 成员,其类型为 struct sigpending,存储着进程接收到的信号队列,struct sigpending 的定义如下:

    struct sigqueue {
        struct sigqueue *next;
        siginfo_t info;
    };
     
    struct sigpending {
        struct sigqueue *head, **tail;
        sigset_t signal;
    };

 当进程接收到一个信号时,就需要把接收到的信号添加 pending 这个队列中。

可以通过 kill() 系统调用发送一个信号给指定的进程,其原型如下:

    int kill(pid_t pid, int sig);

 参数 pid 指定要接收信号进程的ID,而参数 sig 是要发送的信号。kill() 系统调用最终会进入内核态,并且调用内核函数 sys_kill(),代码如下:

    asmlinkage long
    sys_kill(int pid, int sig)
    {
        struct siginfo info;
     
        info.si_signo = sig;
        info.si_errno = 0;
        info.si_code = SI_USER;
        info.si_pid = current->pid;
        info.si_uid = current->uid;
     
        return kill_something_info(sig, &info, pid);
    }

sys_kill() 的代码比较简单,首先初始化 info 变量的成员,接着调用 kill_something_info() 函数来处理发送信号的操作。kill_something_info() 函数的代码如下:

    static int kill_something_info(int sig, struct siginfo *info, int pid)
    {
        if (!pid) {
            return kill_pg_info(sig, info, current->pgrp);
        } else if (pid == -1) {
            int retval = 0, count = 0;
            struct task_struct * p;
     
            read_lock(&tasklist_lock);
            for_each_task(p) {
                if (p->pid > 1 && p != current) {
                    int err = send_sig_info(sig, info, p);
                    ++count;
                    if (err != -EPERM)
                        retval = err;
                }
            }
            read_unlock(&tasklist_lock);
            return count ? retval : -ESRCH;
        } else if (pid < 0) {
            return kill_pg_info(sig, info, -pid);
        } else {
            return kill_proc_info(sig, info, pid);
        }
    }

kill_something_info() 函数根据传入pid 的不同来进行不同的操作,有如下4中可能:

    pid 等于0时,表示信号将送往所有与调用 kill() 的那个进程属同一个使用组的进程。

    pid 大于零时,pid 是信号要送往的进程ID。

    pid 等于-1时,信号将送往调用进程有权给其发送信号的所有进程,除了进程1(init)。

    pid 小于-1时,信号将送往以-pid为组标识的进程。

我们这里只分析 pid 大于0的情况,从上面的代码可以知道,当 pid 大于0时,会调用 kill_proc_info() 函数来处理信号发送操作,其代码如下

    inline int
    kill_proc_info(int sig, struct siginfo *info, pid_t pid)
    {
        int error;
        struct task_struct *p;
     
        read_lock(&tasklist_lock);
        p = find_task_by_pid(pid);
        error = -ESRCH;
        if (p)
            error = send_sig_info(sig, info, p);
        read_unlock(&tasklist_lock);
        return error;
    }

kill_proc_info() 首先通过调用 find_task_by_pid() 函数来获得 pid 对应的进程管理结构,然后通过 send_sig_info()函数来发送信号给此进程,send_sig_info() 函数代码如下:

    int
    send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
    {
        unsigned long flags;
        int ret;
     
        ret = -EINVAL;
        if (sig < 0 || sig > _NSIG)
            goto out_nolock;
     
        ret = -EPERM;
        if (bad_signal(sig, info, t))
            goto out_nolock;
     
        ret = 0;
        if (!sig || !t->sig)
            goto out_nolock;
     
        spin_lock_irqsave(&t->sigmask_lock, flags);
        handle_stop_signal(sig, t);
     
        if (ignored_signal(sig, t))
            goto out;
     
        if (sig < SIGRTMIN && sigismember(&t->pending.signal, sig))
            goto out;
     
        ret = deliver_signal(sig, info, t);
    out:
        spin_unlock_irqrestore(&t->sigmask_lock, flags);
        if ((t->state & TASK_INTERRUPTIBLE) && signal_pending(t))
            wake_up_process(t);
     
    out_nolock:
        return ret;
    }

 send_sig_info() 首先调用 bad_signal() 函数来检查是否有权发送信号给进程,然后调用 ignored_signal() 函数来检查信号是否被忽略,接着调用 deliver_signal() 函数开始发送信号,最后如果进程是睡眠状态就唤醒进程。我们接着来分析 deliver_signal() 函数:

    static int deliver_signal(int sig, struct siginfo *info, struct task_struct *t)
    {
        int retval = send_signal(sig, info, &t->pending);
     
        if (!retval && !sigismember(&t->blocked, sig))
            signal_wake_up(t);
     
        return retval;
    }

 deliver_signal() 首先调用 send_signal() 函数进行信号的发送,然后调用 signal_wake_up() 函数唤醒进程。我们来分析一下最重要的函数 send_signal():

    static int send_signal(int sig, struct siginfo *info, struct sigpending *signals)
    {
        struct sigqueue * q = NULL;
     
        if (atomic_read(&nr_queued_signals) < max_queued_signals) {
            q = kmem_cache_alloc(sigqueue_cachep, GFP_ATOMIC);
        }
     
        if (q) {
            atomic_inc(&nr_queued_signals);
            q->next = NULL;
            *signals->tail = q;
            signals->tail = &q->next;
            switch ((unsigned long) info) {
                case 0:
                    q->info.si_signo = sig;
                    q->info.si_errno = 0;
                    q->info.si_code = SI_USER;
                    q->info.si_pid = current->pid;
                    q->info.si_uid = current->uid;
                    break;
                case 1:
                    q->info.si_signo = sig;
                    q->info.si_errno = 0;
                    q->info.si_code = SI_KERNEL;
                    q->info.si_pid = 0;
                    q->info.si_uid = 0;
                    break;
                default:
                    copy_siginfo(&q->info, info);
                    break;
            }
        } else if (sig >= SIGRTMIN && info && (unsigned long)info != 1
               && info->si_code != SI_USER) {
            return -EAGAIN;
        }
     
        sigaddset(&signals->signal, sig);
        return 0;
    }

send_signal() 函数虽然比较长,但逻辑还是比较简单的。在 信号处理相关的数据结构 一节我们介绍过进程管理结构 task_struct 有个 pending 的成员变量,其用于保存接收到的信号队列。send_signal() 函数的第三个参数就是进程管理结构的 pending 成员变量。

send_signal() 首先调用 kmem_cache_alloc() 函数来申请一个类型为 struct sigqueue 的队列节点,然后把节点添加到 pending 队列中,接着根据参数 info 的值来进行不同的操作,最后通过 sigaddset() 函数来设置信号对应的标志位,表示进程接收到该信号。

signal_wake_up() 函数会把进程的 sigpending 成员变量设置为1,表示有信号需要处理,如果进程是睡眠可中断状态还会唤醒进程。

至此,发送信号的流程已经完成,我们可以通过下面的调用链来更加直观的理解此过程:

    kill() | User Space --------------------------------------------------------------------------------------- | Kernel Space sys_kill() |---> kill_something_info() |---> kill_proc_info() |---> find_task_by_pid() |---> send_sig_info() |---> bad_signal() |---> handle_stop_signal() |---> ignored_signal() |---> deliver_signal() |---> send_signal() | |---> kmem_cache_alloc() | |---> sigaddset() |---> signal_wake_up()

 内核触发信号处理函数

上面介绍了怎么发生一个信号给指定的进程,但是什么时候会触发信号相应的处理函数呢?为了尽快让信号得到处理,Linux把信号处理过程放置在进程从内核态返回到用户态前,也就是在 ret_from_sys_call 处:

    // arch/i386/kernel/entry.S
     
    ENTRY(ret_from_sys_call)
        ...
    ret_with_reschedule:
        ...
        cmpl $0, sigpending(%ebx)  // 检查进程的sigpending成员是否等于1
        jne signal_return          // 如果是就跳转到 signal_return 处执行
    restore_all:
        RESTORE_ALL
     
        ALIGN
    signal_return:
        sti                             // 开启硬件中断
        testl $(VM_MASK),EFLAGS(%esp)
        movl %esp,%eax
        jne v86_signal_return
        xorl %edx,%edx
        call SYMBOL_NAME(do_signal)    // 调用do_signal()函数进行处理
        jmp restore_all

 由于这是一段汇编代码,有点不太直观(大概知道意思就可以了),所以我在代码中进行了注释。主要的逻辑就是首先检查进程的 sigpending 成员是否等于1,如果是调用 do_signal() 函数进行处理,由于 do_signal() 函数代码比较长,所以我们分段来说明,如下:

    int do_signal(struct pt_regs *regs, sigset_t *oldset)
    {
        siginfo_t info;
        struct k_sigaction *ka;
     
        if ((regs->xcs & 3) != 3)
            return 1;
     
        if (!oldset)
            oldset = &current->blocked;
     
        for (;;) {
            unsigned long signr;
     
            spin_lock_irq(&current->sigmask_lock);
            signr = dequeue_signal(&current->blocked, &info);
            spin_unlock_irq(&current->sigmask_lock);
     
            if (!signr)
                break;

上面这段代码的主要逻辑是通过 dequeue_signal() 函数获取到进程接收队列中的一个信号,如果没有信号,那么就跳出循环。我们接着来分析:

    ka = &current->sig->action[signr-1];
            if (ka->sa.sa_handler == SIG_IGN) {
                if (signr != SIGCHLD)
                    continue;
                /* Check for SIGCHLD: it's special.  */
                while (sys_wait4(-1, NULL, WNOHANG, NULL) > 0)
                    /* nothing */;
                continue;
            }

上面这段代码首先获取到信号对应的处理方法,如果对此信号的处理是忽略的话,那么就直接跳过。

    if (ka->sa.sa_handler == SIG_DFL) {
                int exit_code = signr;
     
                /* Init gets no signals it doesn't want.  */
                if (current->pid == 1)
                    continue;
     
                switch (signr) {
                case SIGCONT: case SIGCHLD: case SIGWINCH:
                    continue;
     
                case SIGTSTP: case SIGTTIN: case SIGTTOU:
                    if (is_orphaned_pgrp(current->pgrp))
                        continue;
                    /* FALLTHRU */
     
                case SIGSTOP:
                    current->state = TASK_STOPPED;
                    current->exit_code = signr;
                    if (!(current->p_pptr->sig->action[SIGCHLD-1].sa.sa_flags & SA_NOCLDSTOP))
                        notify_parent(current, SIGCHLD);
                    schedule();
                    continue;
     
                case SIGQUIT: case SIGILL: case SIGTRAP:
                case SIGABRT: case SIGFPE: case SIGSEGV:
                case SIGBUS: case SIGSYS: case SIGXCPU: case SIGXFSZ:
                    if (do_coredump(signr, regs))
                        exit_code |= 0x80;
                    /* FALLTHRU */
     
                default:
                    sigaddset(&current->pending.signal, signr);
                    recalc_sigpending(current);
                    current->flags |= PF_SIGNALED;
                    do_exit(exit_code);
                    /* NOTREACHED */
                }
            }
            ...
            handle_signal(signr, ka, &info, oldset, regs);
            return 1;
        }
        ...
        return 0;
    }

上面的代码表示,如果指定为默认的处理方法,那么就使用系统的默认处理方法去处理信号,比如 SIGSEGV 信号的默认处理方法就是使用 do_coredump() 函数来生成一个 core dump 文件,并且通过调用 do_exit() 函数退出进程。

如果指定了自定义的处理方法,那么就通过 handle_signal() 函数去进行处理,handle_signal() 函数代码如下:

    static void
    handle_signal(unsigned long sig, struct k_sigaction *ka,
              siginfo_t *info, sigset_t *oldset, struct pt_regs * regs)
    {
        ...
        if (ka->sa.sa_flags & SA_SIGINFO)
            setup_rt_frame(sig, ka, info, oldset, regs);
        else
            setup_frame(sig, ka, oldset, regs);
     
        if (ka->sa.sa_flags & SA_ONESHOT)
            ka->sa.sa_handler = SIG_DFL;
     
        if (!(ka->sa.sa_flags & SA_NODEFER)) {
            spin_lock_irq(&current->sigmask_lock);
            sigorsets(&current->blocked,&current->blocked,&ka->sa.sa_mask);
            sigaddset(&current->blocked,sig);
            recalc_sigpending(current);
            spin_unlock_irq(&current->sigmask_lock);
        }

由于信号处理程序是由用户提供的,所以信号处理程序的代码是在用户态的。而从系统调用返回到用户态前还是属于内核态,CPU是禁止内核态执行用户态代码的,那么怎么办?

答案先返回到用户态执行信号处理程序,执行完信号处理程序后再返回到内核态,再在内核态完成收尾工作。听起来有点绕,事实也的确是这样。下面通过一副图片来直观的展示这个过程(图片来源网络):

 

为了达到这个目的,Linux经历了一个十分崎岖的过程。我们知道,从内核态返回到用户态时,CPU要从内核栈中找到返回到用户态的地址(就是调用系统调用的下一条代码指令地址),Linux为了先让信号处理程序执行,所以就需要把这个返回地址修改为信号处理程序的入口,这样当从系统调用返回到用户态时,就可以执行信号处理程序了。

所以,handle_signal() 调用了 setup_frame() 函数来构建这个过程的运行环境(其实就是修改内核栈和用户栈相应的数据来完成)。我们先来看看内核栈的内存布局图:

 

 图中的 eip 就是内核态返回到用户态后开始执行的第一条指令地址,所以把 eip 改成信号处理程序的地址就可以在内核态返回到用户态的时候自动执行信号处理程序了。我们看看 setup_frame() 函数其中有一行代码就是修改 eip 的值,如下:

        static void setup_frame(int sig, struct k_sigaction *ka,
                    sigset_t *set, struct pt_regs * regs)
        {
            ...
            regs->eip = (unsigned long) ka->sa.sa_handler; // regs是内核栈中保存的寄存器集合
            ...
        }

现在可以在内核态返回到用户态时自动执行信号处理程序了,但是当信号处理程序执行完怎么返回到内核态呢?Linux的做法就是在用户态栈空间构建一个 Frame(帧)(我也不知道为什么要这样叫),构建这个帧的目的就是为了执行完信号处理程序后返回到内核态,并恢复原来内核栈的内容。返回到内核态的方式是调用一个名为 sigreturn() 系统调用,然后再 sigreturn() 中恢复原来内核栈的内容。

怎样能在执行完信号处理程序后调用 sigreturn() 系统调用呢?其实跟前面修改内核栈 eip 的值一样,这里修改的是用户栈 eip 的值,修改后跳转到一个执行下面代码的地方(用户栈的某一处):

    popl %eax
    movl $__NR_sigreturn,%eax
    int $0x80

 从上面的汇编代码可以知道,这里就是调用了 sigreturn() 系统调用。修改用户栈的代码在 setup_frame() 中,代码如下:

    static void setup_frame(int sig, struct k_sigaction *ka,
                sigset_t *set, struct pt_regs * regs)
    {
        ...
            err |= __put_user(frame->retcode, &frame->pretcode);
            /* This is popl %eax ; movl $,%eax ; int $0x80 */
            err |= __put_user(0xb858, (short *)(frame->retcode+0));
            err |= __put_user(__NR_sigreturn, (int *)(frame->retcode+2));
            err |= __put_user(0x80cd, (short *)(frame->retcode+6));
        ...
    }

这几行代码比较难懂,其实就是修改信号程序程序返回后要执行代码的地址。修改后如下图:

 

 这样执行完信号处理程序后就会调用 sigreturn(),而 sigreturn() 要做的工作就是恢复原来内核栈的内容了,我们来看看 sigreturn() 的代码:

    asmlinkage int sys_sigreturn(unsigned long __unused)
    {
        struct pt_regs *regs = (struct pt_regs *) &__unused;
        struct sigframe *frame = (struct sigframe *)(regs->esp - 8);
        sigset_t set;
        int eax;

        if (verify_area(VERIFY_READ, frame, sizeof(*frame)))
            goto badframe;
        if (__get_user(set.sig[0], &frame->sc.oldmask)
            || (_NSIG_WORDS > 1
            && __copy_from_user(&set.sig[1], &frame->extramask,
                        sizeof(frame->extramask))))
            goto badframe;

        sigdelsetmask(&set, ~_BLOCKABLE);
        spin_lock_irq(&current->sigmask_lock);
        current->blocked = set;
        recalc_sigpending(current);
        spin_unlock_irq(&current->sigmask_lock);

        if (restore_sigcontext(regs, &frame->sc, &eax))
            goto badframe;
        return eax;

    badframe:
        force_sig(SIGSEGV, current);
        return 0;
    }

最后我们来分析一下怎么设置一个信号处理程序。

用户可以通过 signal() 系统调用设置一个信号处理程序,我们来看看 signal() 系统调用的代码:

    asmlinkage unsigned long
    sys_signal(int sig, __sighandler_t handler)
    {
        struct k_sigaction new_sa, old_sa;
        int ret;

        new_sa.sa.sa_handler = handler;
        new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;

        ret = do_sigaction(sig, &new_sa, &old_sa);

        return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
    }

代码比较简单,就是先设置一个新的 struct k_sigaction 结构,把其 sa.sa_handler 字段设置为用户自定义的处理程序。然后通过 do_sigaction() 函数进行设置,代码如下:

    int
    do_sigaction(int sig, const struct k_sigaction *act, struct k_sigaction *oact)
    {
        struct k_sigaction *k;

        if (sig < 1 || sig > _NSIG ||
            (act && (sig == SIGKILL || sig == SIGSTOP)))
            return -EINVAL;

        k = &current->sig->action[sig-1];

        spin_lock(&current->sig->siglock);

        if (oact)
            *oact = *k;

        if (act) {
            *k = *act;
            sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP));
            
            if (k->sa.sa_handler == SIG_IGN
                || (k->sa.sa_handler == SIG_DFL
                && (sig == SIGCONT ||
                    sig == SIGCHLD ||
                    sig == SIGWINCH))) {
                spin_lock_irq(&current->sigmask_lock);
                if (rm_sig_from_queue(sig, current))
                    recalc_sigpending(current);
                spin_unlock_irq(&current->sigmask_lock);
            }
        }

        spin_unlock(&current->sig->siglock);
        return 0;
    }

     

 这个函数也不难,我们上面介绍过,进程管理结构中有个 sig 的字段,它是一个 struct k_sigaction 结构的数组,每个元素保存着对应信号的处理程序,所以 do_sigaction() 函数就是修改这个信号处理程序。代码 k = &current->sig->action[sig-1] 就是获取对应信号的处理程序,然后把其设置为新的信号处理程序即可。
 

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mybatis从0到1 SQL注入 参数占位符 XML配置 动态SQL

1. Mybatis基础操作 学习完mybatis入门后&#xff0c;我们继续学习mybatis基础操作。 1.1 需求 需求说明&#xff1a; 根据资料中提供的《tlias智能学习辅助系统》页面原型及需求&#xff0c;完成员工管理的需求开发。 通过分析以上的页面原型和需求&#xff0c;我们确定了功能…

我爱学QT-仿写智能家居界面 上 中 下

学习链接&#xff1a; 仿写一个智能家居界面&#xff08;上&#xff09;_哔哩哔哩_bilibili 上 给QT工程添加资源文件 在这里 然后选这个&#xff0c;choose后会有起名&#xff0c;之一千万不能是中文&#xff0c;要不就等报错吧 然后把你要添加的图片托到文件夹下&#xf…

TortoiseGit 入门指南01:环境搭建和软件设置

在我的博文Keil MDK环境下Git入门指南的最后&#xff0c;我这样写道&#xff1a; 目前使用 TortoiseGit 管理工程&#xff0c;用 Gitee 作为远程仓库。 命令行 Git 已经不再使用。 当时我并没有介绍软件 TortoiseGit 的使用方法&#xff0c;这个系列补上。如果你还没有看过《Ke…

希尔排序及其时间复杂度(图文详解)

&#x1f63e; 博客主页: 爱吃bug的猿 &#x1f680;博客专栏: 数据结构,C语言初阶进阶全流程讲解 &#x1f63d;&#x1f63d;&#x1f63d;如果喜欢博主的文章&#xff0c;可以给博主点波赞和关注加速博主更新 文章目录 前言1. 代码思路代码实现法1代码实现法2&#xff08;不…

HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题

导航&#xff1a; 【Java笔记踩坑汇总】Java基础进阶JavaWebSSMSpringBoot瑞吉外卖SpringCloud黑马旅游谷粒商城学成在线MySQL高级篇设计模式常见面试题源码_vincewm的博客-CSDN博客 目录 一、底层 1.1 HashMap数据结构 1.2 扩容机制 1.3 put()流程 1.4 HashMap是如何计算…

【Python爬虫与数据分析】Jupyter的安装与快捷键

目录 一、jupyter notebook安装与配置 二、命令模式快捷键 三、编辑模式快捷键 四、文件操作 一、jupyter notebook安装与配置 安装&#xff1a;Jupyter Notebook是以网页的形式打开的一个程序&#xff0c;集成在Anaconda包里面&#xff0c;也可以直接安装python3解释器&a…

docker安装es集群(三台)

文章目录 1、防火墙设置&#xff0c;开启所需端口2、创建目录&#xff0c;并更改目录权限3 设置系统参数4 启动5 安装ik分词器6 配置7 安装elasticsearch-head&#xff08;用于访问es&#xff0c;界面化工具&#xff09;8、 修改es中每次返回的数据数量参数&#xff08;默认100…

交换机远程登录telnet、SSH、禁止非法用户访问实验

交换机远程登录实验 交换机远程登录实验一、配置Console口登录设备二、配置Telnet远程登录三、Stelnet&#xff08;SSH&#xff09;配置四、远程登录实际中的配置五、禁止非法用户远程登录 交换机远程登录实验 ———————————————————————————————…

汽车+ChatGPT 车内生活体验再升级

这两年&#xff0c;人工智能工具ChatGPT爆火&#xff0c;在全球掀起了大模型之战。如今&#xff0c;最前沿的自然语言处理大模型应用到了人类的出行工具上&#xff0c;梅赛德斯-奔驰和微软官宣正在合作测试车载ChatGPT人工智能&#xff0c;并将面向约90万车主开启测试&#xff…

【操作系统】磁盘调度算法 先来先服务、最短寻道时间优先、扫描算法、循环扫描算法

目录 一、实验要求二、实验原理三、实验内容四、实验结果五、总结 一、实验要求 设计程序模拟先来先服务FCFS、最短寻道时间优先SSTF、扫描算法SCAN和循环扫描算法CSCAN的工作过程。假设有n个磁道号所组成的磁道访问序列&#xff0c;给定开始磁道号m和磁头移动的方向(正向或者反…

Docker 安装Nginx

查看Nginx镜像并拉取镜像&#xff1a; [rootlocalhost nginx]# docker search nginx NAME DESCRIPTION STARS OFFICIAL AUTOMATED nginx …

火热的低代码和无代码赛道

一、背景 星霜荏苒&#xff0c;居诸不息。互联网技术飞速发展&#xff0c;软件的设计、开发、应用也是风发泉涌&#xff0c;无论是开发工具还是应用程序&#xff0c;都在不断追求降本增效&#xff0c;极大地推动了软件研发的长足进步。但然而&#xff0c;长期以来&#xff0c;我…