[linux] linux 常见信号以及使用信号时注意事项

信号是一种进程间通信的方式,进程间通信的方式还包括共享内存,socket,管道,消息队列。信号与其它方式是有区别的,其它通信方式很灵活,可以详细定义自己的消息内容;而信号没有这么灵活,信号的种类在系统中是固定死的,我们可以使用的信号种类,只能从固定的类型中选择。

linux 中的信号有 64 种,可以通过 kill -l 命令查看。

1 信号的使用

信号作为一种进程间通信方式,最基本的有两个处理,一个是发送信号,一个是接收并处理信号。

1.1 kill 和 raise

kill 可以指定要发送的信号,以及要将这个信号发送到哪个或者哪些进程。raise 可以把信号发给进程自己。

kill 函数

int kill(pid_t pid, int sig);

raise 相当于 kill(getpid(), sig)

int raise(int sig);

使用 kill 的时候,我们一般都是向某个进程发送一个信号,第一个形参 pid 就是目标进程的进程号。但是 kill 的 pid 也可以传不大于 0 的数。

pid > 0一个进程
pid == 0信号发向一个进程组,发向哪个进程组呢,就是当前这个进程所在的进程组
pid < -1信号也是发向一个进程组,进程组的组 id 是 pid 的绝对值
pid == -1发向每一个进程,前提这个进程有权限,不会发向 1 号进程

如下代码是 kill 使用的示例,使用 signal 捕获了 SIGTERM 信号,信号处理函数是 signal_handler。父进程中的代码 kill(pid, SIGTERM) 是向子进程发送信号,kill(0, SIGTERM) 是向进程组发信号。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>void signal_handler(int sig_num) {printf("signal = %d, pid = %d\n", sig_num, getpid());
}int main() {signal(SIGTERM, signal_handler);pid_t pid = fork();if (pid == 0) {printf("子进程 id = %d, 进程组 id = %d\n", getpid(), getpgrp());// 子进程while (1) {sleep(1);}} else {printf("父进程 id = %d, 进程组 id = %d\n", getpid(), getpgrp());sleep(2);// kill(pid, SIGTERM);kill(0, SIGTERM);sleep(2);}return 0;
}

1.2 signal 和 sigaction

signal 和 sigaction 都可以用于改变信号的处理方式,指定一个信号,指定这个信号的处理函数,就可以工作了。

当进程中有线程阻塞在内核态的时候,比如 io,这个时候收到信号,默认情况下线程会被唤醒,从阻塞的系统调用中返回。如下代码,使用 sigaction 捕获 SIGTERM 信号,在主函数中有一个 while(1) 循环,循环中使用 scanf() 从标准输入读取数据,我们预期的情况是如果在控制台不输入数据,那么 scanf() 就会一直处于阻塞状态。但是默认的情况和我们预期是不一样的,当向进程发送信号 SIGTERM 时,scanf() 会被唤醒,从而返回。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>void signal_handler(int sig_num) {printf("signal = %d, pid = %d\n", sig_num, getpid());
}int main() {struct sigaction sa_sigterm;sa_sigterm.sa_flags = 0; // SA_RESTART;sa_sigterm.sa_handler = signal_handler;sigaction(SIGTERM, &sa_sigterm, NULL);printf("pid = %d\n", getpid());while (1) {printf("before scanf\n");int a = 10;scanf("%d", &a);printf("after scanf, a = \n", a);}return 0;
}

运行结果如下,当向进程发送 SIGTERM 时,信号处理函数会被调用,同时 scanf() 也会返回。

使用 sigaction 的时候,可以给 sa_flags 设置一个标志 SA_RESTART,设置这个标志之后,收到信号之后 scanf() 就不会返回。

signal 是对 sigaction 的封装,主要 signal 内部使用 sigaction 的时候就设置了 SA_RESTART 标志,所以使用 signal 的话,收到信号的时候 io 阻塞不会返回。在使用 signal 的代码里边,使用 gdb 给 sigaction 设置断点,可以看到 signal 调用了 sigaction,如下图所示。

1.3 signalfd, 一切皆文件

signalfd 是将信号通过一个 fd 来管理,体现了 linux 中一切皆文件的简洁性。

当把一个系统通过文件来管理的时候,往往都要实现自己的 struct file_operations。signalfd 也不例外,如下是实现,其中实现了最主要的两个函数,read 和 poll,通过 read 可以读取信号信息;实现了 poll 函数的话,就支持通过 select,poll,epoll 这些多路复用技术来监听。

static const struct file_operations signalfd_fops = {
#ifdef CONFIG_PROC_FS.show_fdinfo	= signalfd_show_fdinfo,
#endif.release	= signalfd_release,.poll		= signalfd_poll,.read		= signalfd_read,.llseek		= noop_llseek,.may_pollfree	= true,
};

通过  signalfd 来管理信号,要注意以下两点:

(1)使用 read 或者 select, poll, epoll 来监听信号之后,信号的处理就是同步的了;不像通过 signal() 或者 sigaction() 这样直接注册一个信号处理函数,这样是异步处理,有不可重入问题需要注意。改成同步处理之后,我们就可以使用一个单独的线程来处理信号了。

(2)使用 signalfd 来管理信号的时候需要使用 sigprocmask() 来将监听的信号设置为阻塞状态,因为我们并没有改变信号的处理方式,如果不设置为阻塞,那么信号会被默认方式处理,使用 read,select,poll,epoll 就无法获取到信号了。

sigpromask 是设置本线程的 mask,哪个线程调用,就对哪个线程生效。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/signalfd.h>void *thread_function(void *arg) {sigset_t mask;int signal_fd;struct signalfd_siginfo fd_si[8];ssize_t s;sigemptyset(&mask);sigaddset(&mask, SIGTERM);sigaddset(&mask, SIGINT);signal_fd = signalfd(-1, &mask, 0);if (signal_fd == -1) {perror("signa_fd");return NULL;}printf("signal_fd = %d\n", signal_fd);// 信号阻塞,防止信号被默认的处理方式处理if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {perror("sigprocmask");}while (1) {fd_set read_fds;FD_ZERO(&read_fds);FD_SET(signal_fd, &read_fds);sleep(20);printf("before select\n");if (select(signal_fd + 1, &read_fds, NULL, NULL, NULL) == -1) {perror("select");return NULL;}printf("after select\n");s = read(signal_fd, fd_si, 2 * sizeof(struct signalfd_siginfo));if (s == sizeof(struct signalfd_siginfo)) {printf("received signal %d\n", fd_si[0].ssi_signo);} else {printf("received signal %d and %d\n", fd_si[0].ssi_signo, fd_si[1].ssi_signo);}}close(signal_fd);return NULL;
}int main() {pthread_t t1;pthread_create(&t1, NULL, thread_function, NULL);sigset_t mask;sigemptyset(&mask);sigaddset(&mask, SIGTERM);sigaddset(&mask, SIGINT);// 信号阻塞,防止信号被默认的处理方式处理if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {perror("sigprocmask");}printf("before return\n");sleep(200);return 0;
}

1.4 sigtimedwait

与使用 signalfd 类似,sigtimedwait 也可以同步处理信号,并且使用 sigtimedwait 比使用 signalfd 要简洁一些,需要调用的 api 少。

sigtimedwait 和 signalfd 有一个共同的优点,就是可以通过 signal info 获取到信号是谁发送的。这个在实际工作中非常有用,有时候进程被一个信号杀死,我们经常需要确定这个信号是谁发送的。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <errno.h>int main() {sigset_t mask;siginfo_t info;struct timespec timeout;sigemptyset(&mask);sigaddset(&mask, SIGTERM);sigaddset(&mask, SIGINT);// 信号阻塞,防止信号被默认的处理方式处理if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {perror("sigprocmask");}timeout.tv_sec = 5;timeout.tv_nsec = 0;// 在这 20s 期间,向进程分别发送一个 SIGTERM 和 SIGINT 观察怎么处理sleep(20);while (1) {printf("before signal timed wait\n");int result = sigtimedwait(&mask, &info, /*&timeout*/ NULL);printf("after signal timed wait\n");if (result == -1) {if (errno == EAGAIN) {printf("timed out waiting for signal\n");} else {perror("sigtimedwait");return -1;}} else {printf("received signal %d, sent by pid %d\n", result, info.si_pid);}}return 0;
}

1.5 信号是进程的资源

“进程是资源封装的单位”,这句话我们经常看到,信号也是属于进程的资源,所以 struct task_struct 中也有成员来维护这个进程的信号。除了信号之外,进程的资源还包括内存,打开的文件。

struct task_struct 中与信号相关的成员有如下几个:

struct task_struct {/* Signal handlers: */struct signal_struct		*signal; struct sighand_struct __rcu		*sighand; // 信号处理函数sigset_t			blocked; // 线程阻塞哪些信号sigset_t			real_blocked; // 线程当前阻塞的信号struct sigpending		pending; // 当前线程等待处理的信号
}

2 常见信号

在工作中我们经常使用一些信号,比如使用 kill -9 杀死一个进程,ctrl + C 停止一个进程其实是向进程发信号 SIGINT。另外还经常见到一个信号,比如 SIGCHLD,SIGSEGV 等。本节就记录这些常用以及常见的信号。

2.1 SIGKILL

(1)SIGKILL 是信号 9,平时常用 kill -9 杀死一个进程

(2)如果一个进程一直申请内存,导致系统资源紧张的话,系统会用 SIGKILL 将进程杀死

2.2 SIGINT

我们使用最多的 ctrl C 把进程停掉,就是向进程发 SIGINT,2 信号。

2.3 SIGCHILD

当子进程退出的时候会向父进程发送信号 SIGCHLD。

SIGCHLD 默认的操作是 ignore 的。

在开发中也不会专门使用 signal 来捕获 SIGCHLD,而是使用 wait() 来回收子进程。

如下代码可以观察到子进程退出后,父进程收到 SIGCHLD 的现象。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>void signal_handler(int sig_num) {printf("signal = %d, pid = %d\n", sig_num, getpid());
}int main() {signal(SIGCHLD, &signal_handler);printf("pid = %d\n", getpid());pid_t pid = fork();if (pid == 0) {printf("子进程 id %d\n", getpid());sleep(2);} else {printf("父进程 id %d\n", getpid());sleep(5);}return 0;
}

2.4 SIGSEGV

SIGSEGV 是段错误,一般出现在非法访问内存的时候。

如下代码 int a 是一个全局的 const 常量,常量只能在声明的时候初始化,不可以在其他地方修改,没有写权限。可以构造出段错误。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>const int a = 10;
int main() {int *pa = (int *)(void *)(&a);*pa = 100;return 0;
}

2.5 SIGABRT

发送 SIGABRT 信号的原因没有那么具体,进程收到 SIGABRT 的时候,说明发生了严重的错误。

如下代码是对一块内存重复释放了,会产生 SIGABRT 信号。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main() {char *p = (char *)malloc(8);p[0] = 10;free(p);free(p);return 0;
}

2.6 SIGPIPE

当我们使用 tcp 或者 unix socket 中面向连接的 socket 时,如果对端已经关闭了,这个时候我们还向对端发送数据,就会收到 SIGPIPE 信号。

2.7 SIGTERM

systemd 使用了 SIGTERM 信号将 systemd 管理的服务关掉。服务可以捕获这个信号,当收到该信号的时候,说明进程需要关闭,进程内部可以做一些退出前的状态保存工作。

2.8 SIGBUS

使用 mmap 的时候,如果使用不当,会被 SIGBUS 信号杀死。

如下代码,使用 mmap,如果 mmapfile 文件的大小是 0,这个时候通过 p[0] 写数据的话,就会收到  SIGBUS 信号。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {int fd = open("mmapfile", O_RDONLY);if (fd == -1) {perror("open");return 1;}void* map_addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);if (map_addr == MAP_FAILED) {perror("mmap");close(fd);return 1;}char *pc = (char *)map_addr;pc[0] = 10;close(fd);if (munmap(map_addr, 4096) == -1) {perror("munmap");return 1;}return 0;
}

3 信号使用注意事项

3.1 SIGKILL 和 SIGSTOP 不能被用户捕获

signal 的 man 手册中说了,这两个信号不能被用户捕获,也不能忽略。

如下代码分别设置 SIGTERM,SIGKILL, SIGSTOP 的回调函数或者将信号设置为忽略。SIGTERM 可以设置成功,SIGKILL 和 SIGSTOP 设置失败。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>void signal_handler(int sig_num) {printf("signal = %d, pid = %d\n", sig_num, getpid());
}int main() {if (signal(SIGTERM, signal_handler) == SIG_ERR) {perror("capture SIGTERM: ");}if (signal(SIGTERM, SIG_IGN) == SIG_ERR) {perror("ignore SIGTERM: ");}if (signal(SIGKILL, signal_handler) == SIG_ERR) {perror("capture SIGKILL: ");}if (signal(SIGSTOP, signal_handler) == SIG_ERR) {perror("capture SIGTERM: ");}if (signal(SIGKILL, SIG_IGN) == SIG_ERR) {perror("ignore SIGKILL: ");}if (signal(SIGSTOP, SIG_IGN) == SIG_ERR) {perror("ignore SIGSTOP: ");}return 0;
}

运行结果如下:

3.2 一个信号的处理函数在一个进程内只有一个

如果是多线程的程序,在不同的线程里使用 signal() 或者 sigaction() 注册多次信号的回调函数,那么后边注册的会覆盖前边注册的函数,最后一次调用 signal() 或者 sigaction() 传的回调函数是有效的。

当进程收到信号的时候,无论信号被哪个线程处理,都是调用最后这个函数。

如下代码所示,main 函数中首先对 SIGTERM 注册了 signal_handler1 和 signal_handler2 两个函数;然后创建了线程,在线程中又对 SIGTERM 注册了 signal_handler3;最后睡眠 1s 之后,在 main 函数中又注册了 signal_handler4。那么进程收到 SIGTERM 信号之后的处理函数是 signal_handler4。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>void signal_handler1(int sig_num) {printf("1 signal = %d, pid = %d\n", sig_num, getpid());
}void signal_handler2(int sig_num) {printf("2 signal = %d, pid = %d\n", sig_num, getpid());
}void signal_handler3(int sig_num) {printf("3 signal = %d, pid = %d\n", sig_num, getpid());
}void signal_handler4(int sig_num) {printf("4 signal = %d, pid = %d\n", sig_num, getpid());
}void *thread_function(void *arg) {signal(SIGTERM, signal_handler3);sleep(100);
}int main() {signal(SIGTERM, signal_handler1);signal(SIGTERM, signal_handler2);pthread_t t1;pthread_create(&t1, NULL, thread_function, NULL);sleep(1);signal(SIGTERM, signal_handler4);sleep(100);return 0;
}

3.3 fork 之后,子进程与父进程共用一个 signal handler

如下代码,父进程中创建子进程之前使用 signal() 指定信号 SIGTERM 的处理函数是  signal_handler。然后创建一个子进程,在父进程中分别向子进程和父进程中发送信号 SIGTERM,可以看到子进程中的 SIGTERM 处理函数也是 signal_handler。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>void signal_handler(int sig_num) {printf("signal = %d, pid = %d\n", sig_num, getpid());
}int main() {signal(SIGTERM, signal_handler);pid_t pid = fork();if (pid == 0) {printf("子进程 id = %d, 进程组 id = %d\n", getpid(), getpgrp());while (1) {sleep(1);}} else {printf("父进程 id = %d, 进程组 id = %d\n", getpid(), getpgrp());sleep(2);kill(pid, SIGTERM);raise(SIGTERM);sleep(2);}return 0;
}

运行结果如下:

3.3 信号会被并行处理

对于一个多线程的进程来说,如果同一个信号,前一个信号还没有处理完毕,这个时候又来了一个信号,那么两个信号会并发处理吗 ?

会。

如下代码,有两个线程,一个主线程,一个使用 pthread_create() 创建的线程。信号 SIGTERM 的处理函数是 signal_handler。为了构造信号没有处理完毕的场景,在信号处理函数 signal_handler 中是一个死循环,一旦收到了信号,这个函数不返回。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/syscall.h>void signal_handler(int sig_num) {printf("signal = %d, pid = %d, tid = %d\n", sig_num, getpid(), syscall(SYS_gettid));while (1);
}void *thread_function(void *arg) {while (1) {sleep(1);}
}int main() {signal(SIGTERM, signal_handler);pthread_t t1;pthread_create(&t1, NULL, thread_function, NULL);while (1) {sleep(1);}return 0;
}

进程运行之后,连续两次向进程发送信号,可以看到如下的打印,两次处理信号的线程,一次是 10707,一次是 10708。由此可以看出来,同一个信号并不是串行处理的,可以并行处理。

从下边的打印信息也可以看出来,信号处理函数是在线程上下文执行的,并且这个线程是我们自己创建的线程。

3.4 不可重入问题

3.4.1 什么是不可重入问题

信号相对于用户态的线程,类似于中断相对于内核态的线程。

在内核态做开发时,如果数据会被线程和中断同时访问,在线程中我们需要使用关中断自旋锁,在中断处理函数中需要使用自旋锁。这样在线程访问数据的时候,中断是关闭的,所以这个时候中断也上不来,不会打断线程的执行。如果线程中只是加自旋锁,而不关中断,这样会导致问题:因为没有关中断,在线程访问数据的时候,中断是可以打断线程的,这个时候中断要获取锁获取不到(因为自旋锁被线程拿着),中断一直无法返回,中断无法返回就会导致线程也得不到执行,这样就造成了中断和线程之间的死锁。

内核线程:

spin_lock_irq(); // 关中断自旋锁
// do something
spin_unloc_irq();

中断处理函数:

spin_lock();
// do something
spin_unlock();

什么是信号的重入问题呢 ?

如下代码,有一个全局变量 g_a,这个变量会在两处修改,一个是线程 t1 中,一个是 SIGINT 的信号处理函数 signal_handler 中。如果只是在多个线程中处理,不在信号中处理,这就是我们常见的多线程并发问题,多线程并发问题可以在访问变量的地方加锁来解决。但是如果这个变量在线程中处理,也在信号中处理,就不能使用自旋锁来解决了。从上边的分析也能看出来,信号回调函数的调用是在线程上下文,并且这个调用的时机是不确定的,那就很有可能出现线程正在拿着自旋锁,这个时候线程被打断,而这个时候信号处理函数中也想拿到自旋锁,但是自旋锁被线程拿着,所以信号处理函数也拿不到自旋锁,就产生了死锁。这就是信号的重入问题,使用自旋锁无法解决。我们经常使用的 malloc(),free(),printf() 这些标准库函数都是不可重入的,也就是不能在信号处理函数中调用这些函数。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>int g_a = 0;void signal_handler(int signum) {printf("received signal: %d\n", signum);// pthrtead_spin_lock()g_a++;// pthread_spin_unlock()
}void *thread_function(void *arg) {while (1) {// pthread_spin_lock()g_a++;// pthread_spin_unlock()sleep(1);}
}int main() {signal(SIGINT, signal_handler);pthread_t t1;pthread_create(&t1, NULL, thread_function, NULL);while (1) {sleep(1);}return 0;
}

3.4.2 怎么避免不可重入问题

(1)直接调用系统调用,而不是标准库函数

以 printf() 为例,printf() 是向标准输出打印信息,标准输出的 fd 是 1,我们可以不使用 printf(),而是使用 write 系统调用直接向文件 1 写内容。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>int main() {printf("hello\n");char *str = "hello world\n";write(1, str, strlen(str));return 0;
}

运行结果如下:

(2)使用 signalfd 或者 sigtimedwait,将不可重入问题改成并发问题

改成并发问题就能通过加锁来解决了

(3)参考内核线程中的关中断自旋锁方案

在线程加锁之前使用 sigprocmast() 函数将信号阻塞,SIG_BLOCK;处理完毕之后,解除信号阻塞,SIG_UNBLOCK。

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

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

相关文章

7款炫酷的前端动画特效分享(二)(附效果图及在线演示)

分享7款好玩的前端动画特效 其中有CSS动画、SVG动画、js小游戏等等 下方效果图可能不是特别的生动 那么你可以点击在线预览进行查看相应的动画特效 同时也是可以下载该资源的 jQuery拉开帷幕特效 基于jQuery实现的帷幕特效 点击右侧拉条 可以实现帷幕的收起也展开 非常的炫酷…

Doris实战——金融壹账通指标中台的应用实践

目录 前言 一、业务痛点 二、早期架构挑战 三、架构升级 四、一体化指标数据平台 4.1 构建指标体系 4.2 构建指标平台功能 五、Doris指标应用实践 六、未来规划 原文大佬的这篇指标中台的应用实践有借鉴意义&#xff0c;这里摘抄下来用作学习和知识沉淀。 前言 在搭建…

第四十七回 一丈青单捉王矮虎 宋公明二打祝家庄-强大而灵活的python装饰器

四面全是埋伏&#xff0c;宋江和众人一直绕圈跑不出去。正在慌乱之时&#xff0c;石秀及时赶到&#xff0c;教大家碰到白杨树就转弯走。走了一段时间&#xff0c;发现围的人越来越多&#xff0c;原来祝家庄以灯笼指挥号令。花荣一箭射下来红灯龙&#xff0c;伏兵自己就乱起来了…

【DAY07 软考中级备考笔记】数据结构:线性结构,数组矩阵和广义表

数据结构&#xff1a;线性结构&#xff0c;数组矩阵和广义表 3月2日 – 天气&#xff1a;晴 1. 线性表的定义和存储方式 > 这一部分只需要掌握下面的两点即可&#xff1a; > > * 采用顺序存储和链式存储的特点 > * 单链表的插入和删除操作 2. 栈和队列 > 这里需…

Godot自定义控件样式语法解析

前言 本篇原始文章写于2023年8月7日&#xff0c;存储在我的语雀文档中。但是语雀分享有诸多不便&#xff0c;为了让更多Godoter更轻松的搜到和看到&#xff0c;就转过来了。 这个项目我上传了Github&#xff0c;后续会贴上链接。 概述 Godot控件体系存在的问题之一就是样式无…

无人值守,24小时水质在线检测系统,助力信息化平台建设

水质在线监测系统主要由在线自动检测系统、通信网络和监控中心三部分组成&#xff0c;可以在无人值守的情况下自动完成水样的采集、水质分析、数据的采集和通信传输。 主要功能&#xff1a;监测水厂出口&#xff0c;输水管线和终端的水质情况。具体要求&#xff1a;地图显示。…

elegentbook模板不生成目录的解决方法

这里只有目录两个字、却没有生成目录 在json里面修改 "latex-workshop.latex.autoClean.run": "onBuilt",把onBuilt改为onFailed即可 "latex-workshop.latex.autoClean.run": "onFailed",

正压式采样器——气体采样器

不管路有多远&#xff0c;只要你不停步&#xff0c;总会在你的脚下。无论志向有多高&#xff0c;只要你不放弃&#xff0c;总会在你的胸怀。不要等待机会&#xff0c;而是创造机会。只有走出来的美丽&#xff0c;没有等出来的辉煌。 正压采样器的用途&#xff1a; 该正压采样器…

php连接hdfs初步探索

一、phdfs拓展 结果&#xff1a;暂时舍弃 安装此拓展时&#xff0c;无法make成功&#xff0c;因为缺少hdfs.n文件。 换了其他版本的拓展包&#xff0c;并编译都没有找到此文件。 后搜到官网的相关资料&#xff0c;此hdfs.h的文件路径的地址是$HADOOP_HDFS_HOME/include/hdfs…

【计算机网络_应用层】协议定制序列化反序列化

文章目录 1. TCP协议的通信流程2. 应用层协议定制3. 通过“网络计算器”的实现来实现应用层协议定制和序列化3.1 protocol3.2 序列化和反序列化3.2.1 手写序列化和反序列化3.2.2 使用Json库 3.3 数据包读取3.4 服务端设计3.5 最后的源代码和运行结果 1. TCP协议的通信流程 在之…

安装OPC报1603错误的处理

因为本电脑调试过OPC Client软件&#xff0c;设置过DCOM&#xff0c;待安装OPC Server时报1603错误&#xff1a; 研究颇久&#xff0c;后来发现修改一下dcom配置就好了&#xff1a; 运行 dcomcnfg&#xff0c;将“我的电脑”属性修改如下&#xff1a;

自注意力机制(Self-Attention)

注意&#xff1a;本文引用自专业人工智能社区Venus AI 更多AI知识请参考原站&#xff08;【http://www.aideeplearning.cn】&#xff09; Transformer模型中最关键部分就是自注意力&#xff08;Self-Attention&#xff09;机制&#xff0c;正如 Transformer 的论文的标题是“…