Linux/AndroidOS中进程间的通信线程间的同步 - 管道和FIFO
前言
管道是 UNIX 系统上最古老的 IPC 方法,它在 20 世纪 70 年代早期 UNIX 的第三个版本上就出现了。管道为一个常见需求提供了一个优雅的解决方案:给定两个运行不同程序的进程,在 shell 中如何让一个进程的输出作为另一个进程的输入呢?管道可以用来在相关进程之间传递数据。FIFO 是管道概念的一个变体,它们之间的一个重要差别在于 FIFO 可以用于任意进程间的通信。
1 概述
每个 shell 用户都对在命令中使用管道比较熟悉,如下面这个统计一个目录中文件的数目的命令所示。
ls | wc -l
为执行上面的命令,shell 创建了两个进程来分别执行 ls 和 wc。(这是通过使用 fork()和exec()来完成的)下图展示了这两个进程是如何使用管道的。
两个进程都连接到了管道上,这样写入进程(ls)就将其标准输出(文件描述符为 1)连接到了管道的写入端,读取进程(wc)就将其标准输入(文件描述符为 0)连接到管道的读取端。实际上,这两个进程并不知道管道的存在,它们只是从标准文件描述符中读取数据和写入数据。
1.1 一个管道是一个字节流
- 管道是一个字节流,在使用管道时是不存在消息或消息边界的概念的。从管道中读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是什么。
- 通过管道传递的数据是顺序的——从管道中读取出来的字节的顺序与它们被写入管道的顺序是完全一样的。在管道中无法使用lseek()来随机地访问数据。
1.2 从管道中读取数据
- 试图从一个当前为空的管道中读取数据将会被阻塞直到至少有一个字节被写入到管道中为止。
- 如果管道的写入端被关闭了,那么从管道中读取数据的进程在读完管道中剩余的所有数据之后将会看到文件结束(即 read()返回 0)。
1.3 管道是单向的
在管道中数据的传递方向是单向的。管道的一端用于写入,另一端则用于读取。
1.4 管道的容量是有限的
管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的。一旦管道被填满之后,后续向该管道的写入操作就会被阻塞直到读者从管道中移除了一些数据为止。
一般来讲,一个应用程序无需知道管道的实际存储能力。如果需要防止写者进程阻塞,那么从管道中读取数据的进程应该被设计成以尽可能快的速度从管道中读取数据。
2 创建和使用管道
pipe()系统调用创建一个新管道。
# include <unistd.h>
int pipe(int filedes[2]);
成功的 pipe()调用会在数组 filedes 中返回两个打开的文件描述符:一个表示管道的读取端(filedes[0]),另一个表示管道的写入端(filedes[1])。
与所有文件描述符一样,可以使用 read()和 write()系统调用来在管道上执行 I/O。一旦向管道的写入端写入数据之后立即就能从管道的读取端读取数据。管道上的 read()调用会读取的数据量为所请求的字节数与管道中当前存在的字节数两者之间较小的那个(但当管道为空时阻塞)。
也可以在管道上使用 stdio 函数(printf()、scanf()等),只需要首先使用 fdopen()获取一个与 filedes 中的某个描述符对应的文件流即可。
下图给出了使用 pipe()创建完管道之后的情况,其中调用进程通过文件描述符引用了管道的两端。
在单个进程中管道的用途不多。一般来讲都是使用管道让两个进程进行通信。为了让两个进程通过管道进行连接,在调用完 pipe()之后可以调用 fork()。在fork()期间,子进程会继承父进程的文件描述符的副本,这样就会出现下图中左边那样的情形。
虽然父进程和子进程都可以从管道中读取和写入数据,但这种做法并不常见。因此,在fork()调用之后,其中一个进程应该立即关闭管道的写入端的描述符,另一个则应该关闭读取端的描述符。如果父进程需要向子进程传输数据,那么它就会关闭管道的读取端的描述符 filedes[0],而子进程就会关闭管道的写入端的描述符 filedes[1],这样就出现了下图中右边那样的情形。
int filedes[2];
if(pipe(filedes)== -1) /* Create the pipe */errExit("pipe”);switch(fork()){ /*Create a child process */
case -1:errExit("fork”);
case 0: /*Child */if(close(filedes[1]) == -1) /* Close unused write end */errExit("close");/*Child now reads from pipe */break;
default: /*Parent */if(close(filedes[o]) == -1) /*Close unused read end */errExit("close");/* Parent now writes to pipe */break ;
让父进程和子进程都能够从同一个管道中读取和写入数据这种做法并不常见的一个原因是如果两个进程同时试图从管道中读取数据,那么就无法确定哪个进程会首先读取成功—两个进程竞争数据了。要防止这种竞争情况的出现就需要使用某种同步机制。
但如果需要双向通信则可以使用一种更加简单的方法:创建两个管道,在两个进程之间发送数据的两个方向上各使用一个。(如果使用这种技术,那么就需要考虑死锁的问题了,因为如果两个进程都试图从空管道中读取数据或尝试向已满的管道中写入数据就可能会发生死锁。)
虽然可以有多个进程向单个管道中写入数据,但通常只存在一个写者。
2.1 管道允许相关进程间的通信
目前为止本章已经介绍了如何使用管道来让父进程和子进程之间进行通信,其实管道可以用于任意两个(或更多)相关进程之间的通信,只要在创建子进程的系列 fork()调用之前通过一个共同的祖先进程创建管道即可。如管道可用于一个进程和其孙子进程之间的通信。
第一个进程创建管道,然后创建子进程,接着子进程再创建第一个进程的孙子进程。管道通常用于两个兄弟进程之间的通信——它们的父进程创建了管道,然后创建两个子进程。这就是在构建管道线时 shell所做的工作。
2.2 关闭未使用管道文件描述符
关闭未使用管道文件描述符不仅仅是为了确保进程不会耗尽其文件描述符的限制——这对于正确使用管道是非常重要的。
从管道中读取数据的进程会关闭其持有的管道的写入描述符,这样当其他进程完成输出并关闭其写入描述符之后,读者就能够看到文件结束(在读完管道中的数据之后)。
- 如果读取进程没有关闭管道的写入端,那么在其他进程关闭了写入描述符之后,读者也不会看到文件结束,即使它读完了管道中的所有数据。
- 相反,read()将会阻塞以等待数据,这是因为内核知道至少还存在一个管道的写入描述符打开着,即读取进程自己打开了这个描述符。从理论上来讲,这个进程仍然可以向管道写入数据,即使它已经被读取操作阻塞了。如read()可能会被一个向管道写入数据的信号处理器中断。
写入进程关闭其持有的管道的读取描述符是出于不同的原因
-
当一个进程试图向一个管道中写入数据但没有任何进程拥有该管道的打开着的读取描述符时,内核会向写入进程发送一个 SIGPIPE 信号。在默认情况下,这个信号会杀死一个进程。但进程可以捕获或忽略该信号,这样就会导致管道上的 write()操作因 EPIPE 错误(已损坏的管道)而失败。收到 SIGPIPE信号或得到 EPIPE 错误对于标示出管道的状态是有用的,这就是为何需要关闭管道的未使用读取描述符的原因。
-
如果写入进程没有关闭管道的读取端,那么即使在其他进程已经关闭了管道的读取端之后写入进程仍然能够向管道写入数据,最后写入进程会将数据充满整个管道,后续的写入请求会被永远阻塞。
-
关闭未使用文件描述符的最后一个原因是只有当所有进程中所有引用一个管道的文件描述符被关闭之后才会销毁该管道以及释放该管道占用的资源以供其他进程复用。此时,管道中所有未读取的数据都会丢失。
2.3 示例程序
程序演示了如何将管道用于父进程和子进程之间的通信。这个例子演示了前面提及的管道的字节流特性——父进程在一个操作中写入数据,子进程一小块一小块地从管道中读取数据。
- 主程序调用 pipe()创建管道,然后调用 fork()创建一个子进程。
- 在 fork()调用之后,父进程关闭了其持有的管道的读取端的文件描述符并将通过程序的命令行参数传递进来的字符串写到管道的写入端。
- 父进程接着关闭管道的读取端并调用 wait()等待子进程终止 。
- 在关闭了所持有的管道的写入端的文件描述符之后,子进程进入了一个循环,在这个循环中从管道读取数据块并将它们写到⑥标准输出中。
- 当子进程碰到管道的文件结束时就退出循环,并写入一个结尾换行字符以及关闭所持有的管道的读取端的描述符,最后终止。
下面是运行程序时可能看到的输出。
./simple_pipe 'It was a bright cold day in April,and the clocks were striking thirteen.'
It was a bright cold day in April,and the clocks were striking thirteen.
#include <sys/wait.h>
#include "tlpi_hdr.h"#define BUF_SIZE 10int
main(int argc, char *argv[])
{int pfd[2]; /* Pipe file descriptors */char buf[BUF_SIZE];ssize_t numRead;if (argc != 2 || strcmp(argv[1], "--help") == 0)usageErr("%s string\n", argv[0]);if (pipe(pfd) == -1) /* Create the pipe */errExit("pipe");switch (fork()) {case -1:errExit("fork");case 0: /* Child - reads from pipe */if (close(pfd[1]) == -1) /* Write end is unused */errExit("close - child");for (;;) { /* Read data from pipe, echo on stdout */numRead = read(pfd[0], buf, BUF_SIZE);if (numRead == -1)errExit("read");if (numRead == 0)break; /* End-of-file */if (write(STDOUT_FILENO, buf, numRead) != numRead)fatal("child - partial/failed write");}write(STDOUT_FILENO, "\n", 1);if (close(pfd[0]) == -1)errExit("close");exit(EXIT_SUCCESS);default: /* Parent - writes to pipe */if (close(pfd[0]) == -1) /* Read end is unused */errExit("close - parent");if (write(pfd[1], argv[1], strlen(argv[1])) != strlen(argv[1]))fatal("parent - partial/failed write");if (close(pfd[1]) == -1) /* Child will see EOF */errExit("close");wait(NULL); /* Wait for child to finish */exit(EXIT_SUCCESS);}
}
3 将管道作为一种进程同步的方法
这个程序创建了多个子进程(每个命令行参数对应一个子进程),每个子进程都完成某个动作,在本例中则是睡眠一段时间。父进程等待直到所有子进程完成了自己的动作为止。为了执行同步:
- 父进程在创建子进程之前构建了一个管道。
- 每个子进程会继承管道的写入端的文件描述符并在完成动作之后关闭这些描述符。
- 当所有子进程都关闭了管道的写入端的文件描述符之后,父进程在管道上的 read()就会结束并返回文件结束(0)。
这时,父进程就能够做其他工作了。(注意在父进程中关闭管道的未使用写入端对于这项技术的正常运转是至关重要的,否则父进程在试图从管道中读取数据时会被永远阻塞。)
下面是创建三个分别睡眠 4、2 和 6 秒的子进程时所看到的输出。
./pipe_sync 4 2 6
00:59:08 Parent started
00:59:10 Child 2 (PID=181182) closing pipe
00:59:12 Child 1 (PID=181181) closing pipe
00:59:14 Child 3 (PID=181183) closing pipe
00:59:14 Parent ready to go
#include "curr_time.h" /* Declaration of currTime() */
#include "tlpi_hdr.h"int
main(int argc, char *argv[])
{int pfd[2]; /* Process synchronization pipe */int j, dummy;if (argc < 2 || strcmp(argv[1], "--help") == 0)usageErr("%s sleep-time...\n", argv[0]);setbuf(stdout, NULL); /* Make stdout unbuffered, since weterminate child with _exit() */printf("%s Parent started\n", currTime("%T"));if (pipe(pfd) == -1)errExit("pipe");for (j = 1; j < argc; j++) {switch (fork()) {case -1:errExit("fork %d", j);case 0: /* Child */if (close(pfd[0]) == -1) /* Read end is unused */errExit("close");/* Child does some work, and lets parent know it's done */sleep(getInt(argv[j], GN_NONNEG, "sleep-time"));/* Simulate processing */printf("%s Child %d (PID=%ld) closing pipe\n",currTime("%T"), j, (long) getpid());if (close(pfd[1]) == -1)errExit("close");/* Child now carries on to do other things... */_exit(EXIT_SUCCESS);default: /* Parent loops to create next child */break;}}/* Parent comes here; close write end of pipe so we can see EOF */if (close(pfd[1]) == -1) /* Write end is unused */errExit("close");/* Parent may do other work, then synchronizes with children */if (read(pfd[0], &dummy, 1) != 0)fatal("parent didn't get EOF");printf("%s Parent ready to go\n", currTime("%T"));/* Parent can now carry on to do other things... */exit(EXIT_SUCCESS);
}
与使用信号来同步相比,使用管道同步具备一个优势:它可以同来协调一个进程的动作使之与多个其他(相关)进程匹配。而多个(标准)信号无法排队的事实使得信号不适用于这种情形。(相反,信号的优势是它可以被一个进程广播到进程组中的所有成员处。)
其他同步结构也是可行的(如使用多个管道)。此外,还可以对这项技术进行扩展,即不关闭管道,每个子进程向管道写入一条包含其进程 ID 和一些状态信息的消息。或者每个子进程可以向管道写入一个字节。父进程可以计数和分析这些消息。这种方法考虑到了子进程意外终止而不是显式地关闭管道的情形。
4 使用管道连接过滤器
当管道被创建之后,为管道的两端分配的文件描述符是可用描述符中数值最小的两个。由于在通常情况下,进程已经使用了描述符 0、1 和 2,因此会为管道分配一些数值更大的描述符。那么如何形成图 44-1 中给出的情形呢,使用管道连接两个过滤器(即从 stdin 读取和写入到 stdout的程序)使得一个程序的标准输出被定向到管道中,而另一个程序的标准输入则从管道中读取?特别是如何在不修改过滤器本身的代码的情况下完成这项工作呢?
这个问题的答案是使用在 5.5 节中介绍的技术,即复制文件描述符。一般来讲会使用下面的系列调用来获得预期的结果。
int pfd[2];
pipe(pfd); /*Allocates(say)file descriptors 3 and 4 for pipe *//* other steps here,e.g.,fork()*/
close(STDOUT_FILENO); /* Free file descriptor 1 */
dup(pfd[1]); /* Duplication uses lowest free file descriptor,i.e.,fd 1 */
上面这些调用的最终结果是进程的标准输出被绑定到了管道的写入端。而对应的一组调用可以用来将进程的标准输入绑定到管道的读取端上。
注意,上面这些调用假设已经为进程打开了文件描述符 0、1 和 2。(shell 通常能够确保为它执行的每个程序都打开了这三个描述符。)如果在执行上面的调用之前文件描述符 0 已经被关闭了,那么就会错误地将进程的标准输入绑定到管道的写入端上。为避免这种情况的发生,可以使用 dup2()调用来取代对 close()和 dup()的调用,因为通过这个函数可以显式地指定被绑定到管道一端的描述符。
dup2(pfd[1],STDOUT_FILENO); /*Close descriptor 1,and reopen boundto write end of pipe */
在复制完 pfd[1]之后就拥有两个引用管道的写入端的文件描述符了:描述符 1 和 pfd[1]。由于未使用的管道文件描述符应该被关闭,因此在 dup2()调用之后需要关闭多余的描述符。
close(pfd[1]);
前面给出的代码依赖于标准输出在之前已经被打开这个事实。假设在 pipe()调用之前,标准输入和标准输出都被关闭了。那么在这种情况下,pipe()就会给管道分配这两个描述符,即 pfd[0]的值可能为 0,pfd[1]的值可能为 1。其结果是前面的 dup2()和 close()调用将下面的代码等价。
dup2(1,1); /* Does nothing */
close(1); /*Closes sole descriptor for write end of pipe */
因此按照防御性编程实践的要求最好将这些调用放在一个 if 语句中,如下所示。
if(pfd[1]!= STDOUT_FILENO){dup2(pfd[1],STDOUT_FILENO);close(pfd[1]);
}
示例程序(使用管道连接ls和wc)
在构建完一个管道之后,这个程序创建了两个子进程。第一个子进程将其标准输出绑定到管道的写入端,然后执行 ls。第二个子进程将其标准输入绑定到管道的写入端,然后执行 wc。
#include <sys/wait.h>
#include "tlpi_hdr.h"int
main(int argc, char *argv[])
{int pfd[2]; /* Pipe file descriptors */if (pipe(pfd) == -1) /* Create pipe */errExit("pipe");switch (fork()) {case -1:errExit("fork");case 0: /* First child: exec 'ls' to write to pipe */if (close(pfd[0]) == -1) /* Read end is unused */errExit("close 1");/* Duplicate stdout on write end of pipe; close duplicated descriptor */if (pfd[1] != STDOUT_FILENO) { /* Defensive check */if (dup2(pfd[1], STDOUT_FILENO) == -1)errExit("dup2 1");if (close(pfd[1]) == -1)errExit("close 2");}execlp("ls", "ls", (char *) NULL); /* Writes to pipe */errExit("execlp ls");default: /* Parent falls through to create next child */break;}switch (fork()) {case -1:errExit("fork");case 0: /* Second child: exec 'wc' to read from pipe */if (close(pfd[1]) == -1) /* Write end is unused */errExit("close 3");/* Duplicate stdin on read end of pipe; close duplicated descriptor */if (pfd[0] != STDIN_FILENO) { /* Defensive check */if (dup2(pfd[0], STDIN_FILENO) == -1)errExit("dup2 2");if (close(pfd[0]) == -1)errExit("close 4");}execlp("wc", "wc", "-l", (char *) NULL);errExit("execlp wc");default: /* Parent falls through */break;}/* Parent closes unused file descriptors for pipe, and waits for children */if (close(pfd[0]) == -1)errExit("close 5");if (close(pfd[1]) == -1)errExit("close 6");if (wait(NULL) == -1)errExit("wait 1");if (wait(NULL) == -1)errExit("wait 2");exit(EXIT_SUCCESS);
}
执行程序会看到下面的输出:
./pipe_ls_wc
18
ls | wc -l
18
5 FIFO
从语义上来讲,FIFO 与管道类似,它们两者之间最大的差别在于 FIFO 在文件系统中拥有一个名称,并且其打开方式与打开一个普通文件是一样的。这样就能够将 FIFO 用于非相关进程之间的通信(如客户端和服务器)。
一旦打开了 FIFO,就能在它上面使用与操作管道和其他文件的系统调用一样的 I/O 系统调用了(如 read()、write()和 close())。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。FIFO 有时候也被称为有名管道。
与管道一样,当所有引用 FIFO 的描述符都被关闭之后,所有未被读取的数据会被丢弃。使用 mkfifo 命令可以在 shell 中创建一个 FIFO。
mkfifo [-m mode] pathname
pathname 是创建的 FIFO 的名称,–m 选项用来指定权限 mode,其工作方式与 chmod 命令一样。
当在 FIFO(或管道)上调用 fstat()和 stat()函数时它们会在 stat 结构的 st_mode 字段中返回一个类型为 S_IFIFO 的文件。当使用 ls –l 列出文件时,FIFO 文件在第一列的类型为 p,ls -F 会在 FIFO 路径名后面附加上一个管道符(|)。
$ mkfifo mxr
$ ls mxr -al
prw-rw-r-- 1 maxingrong maxingrong 0 Apr 29 05:29 mxr$ ls -F mxr
mxr|
mkfifo()函数创建一个名为 pathname 的全新的 FIFO。
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
一般来讲,使用 FIFO 时唯一明智的做法是在两端分别设置一个读取进程和一个写入进程。这样在默认情况下:
- 打开一个 FIFO 以便读取数据(open() O_RDONLY 标记)将会阻塞直到另一个进程打开 FIFO 以写入数据(open() O_WRONLY 标记)为止。
- 打开一个 FIFO 以写入数据将会阻塞直到另一个进程打开FIFO 以读取数据为止。
换句话说,打开一个 FIFO 会同步读取进程和写入进程。如果一个 FIFO的另一端已经打开(可能是因为一对进程已经打开了 FIFO 的两端),那么 open()调用会立即成功。
在大多数 UNIX 实现(包括 Linux)上,当打开一个 FIFO 时可以通过指定 O_RDWR 标记来绕过打开 FIFO 时的阻塞行为。这样,open()就会立即返回,但无法使用返回的文件描述符在 FIFO 上读取和写入数据。这种做法破坏了 FIFO 的 I/O 模型,对于那些需要避免在打开 FIFO 时发生阻塞的需求,open()的 O_NONBLOCK 标记提供了一种标准化的方法来完成这个任务。
使用FIFO和tee(1)创建双重管道线
shell 管道线的其中一个特征是它们是线性的,管道线中的每个进程都读取前一个进程产生的数据并将数据发送到其后一个进程中。使用 FIFO 就能够在管道线中创建子进程,这样除了将一个进程的输出发送给管道线中的后面一个进程之外,还可以复制进程的输出并将数据发送到另一个进程中。要完成这个任务需要使用 tee 命令,它将其从标准输入中读取到的数据复制两份并输出:一份写入到标准输出,另一份写入到通过命令行参数指定的文件中。
将传给tee命名的file参数设置为一个FIFO可以让两个进程同时读取tee产生的两份数据。下面的 shell 会话演示了这种用法,它创建了一个名为 mxr 的 FIFO,然后在后台启动一个wc 命令,该命令会打开 FIFO 以读取数据(这个操作会阻塞直到有进程打开 FIFO 写入数据为止),接着执行一条管道线将 ls 的输出发送给 tee,tee 会将输出传递给管道线中的下一个命令sort,同时还会将输出发送给名为 myfifo 的 FIFO。(sort 的–k5n 选项会导致 ls 的输出按照第五个以空格分隔的字段的数值升序排序。)
mkfifo mxr
wc -l < mxr & #等待从mxr读数据
ls -l | tee mxr |sort -k5n
6 使用管道实现一个客户端/服务器应用程序
本节将介绍一个简单的使用 FIFO 进行 IPC 的客户端/服务器应用程序。服务器提供的(简单)服务是向每个发送请求的客户端赋一个唯一的顺序数字。在对这个应用程序进行讨论的过程中将会介绍与服务器设计有关的一些概念和技术。
6.1 应用程序概述
在这个示例应用程序中,所有客户端使用一个服务器 FIFO 来向服务器发送请求。头文件pipes/fifo_seqnum.h定义了众所周知的名称(/tmp/seqnum_sv),服务器的 FIFO 将使用这个名称。这个名称是固定的,因此所有客户端知道如何联系到服务器。
在这个示例应用程序中将会在/tmp 目录中创建 FIFO,这样在大多数系统上都能够在不修改程序的情况下方便地运行这个程序。在一个像/tmp 这样的公共可写的目录中创建文件可能会导致各种安全隐患,因此现实世界中的应用程序不应该使用这种目录。
无法使用单个 FIFO 向所有客户端发送响应,因为多个客户端在从 FIFO 中读取数据时会相互竞争,这样就可能会出现各个客户端读取到了其他客户端的响应消息,而不是自己的响应消息。因此每个客户端需要创建一个唯一的 FIFO,服务器使用这个 FIFO 来向该客户端递送响应,并且服务器需要知道如何找出各个客户端的 FIFO。
解决这个问题的方式是:
- 让客户端生成自己的 FIFO 路径名,然后将路径名作为请求消息的一部分传递给服务器。
- 客户端和服务器可以约定一个构建客户端 FIFO 路径名的规则,然后客户端可以将构建自己的路径名所需的相关信息作为请求的一部分发送给服务器。
本例中将会使用后面一种解决方案。每个客户端的 FIFO 是从一个由包含客户端的进程 ID 的路径名构成的模板(CLIENT_FIFO_TEMPLATE)中构建而来的。在生成过程中包含进程 ID 可以很容易地产生一个对各个客户端唯一的名称。
下图展示了这个应用程序如何使用 FIFO 来完成客户端和服务器进程之间的通信。
头文件 pipes/fifo_seqnum.h 定义了客户端发送给服务器的请求消息的格式和服务器发送给客户端的响应消息的格式。
记住管道和 FIFO 中的数据是字节流,消息之间是没有边界的。这意味着当多条消息被递送到一个进程中时,如本例中的服务器,发送者和接收者必须要约定某种规则来分隔消息。这可以使用多种方法:
- 每条消息使用诸如换行符之类的分隔字符结束。这样就必须要保证分隔字符不会出现在消息中或者当它出现在消息中时必须要采用某种规则进行转义。例如,如果使用换行符作为分隔符,那么字符\加上换行可以用来表示消息中一个真实的换行符,而\则可以用来表示一个真实的\。这种方法的一个不足之处是读取消息的进程在从 FIFO 中扫描数据时必须要逐个字节地分析直到找到分隔符为止。
- 在每条消息中包含一个大小固定的头,头中包含一个表示消息长度的字段,该字段指定了消息中剩余部分的长度。这样读取进程就需要首先从 FIFO 中读取头,然后使用头中的长度字段来确定需读取的消息中剩余部分的字节数。这种方法能够高效地读取任意大小的消息,但一旦不合规则(如错误的 length 字段)的消息被写入到管道中之后问题就出来了。
- 使用固定长度的消息并让服务器总是读取这个大小固定的消息。这种方法的优势在于简单性。但它对消息的大小设置了一个上限,意味着会浪费一些通道容量(因为需要对较短的消息进行填充以满足固定长度)。此外,如果其中一个客户端意外地或故意发送了一条长度不对的消息,那么所有后续的消息都会出现步调不一致的情况,并且在这种情况下服务器是难以恢复的。
示例应用程序中将使用上面介绍的第三种技术,即每个客户端向服务器发送的消息的长度是固定的。代码中request 结构定义了消息。每个发送给服务器的请求都包含了客户端的进程 ID,这样服务器就能够构建客户端用来接收响应的 FIFO 的名称了。请求中还包含了一个 seqLen 字段,它指定了应该为这个客户端分配的序号的数量。服务器向客户端发送的响应消息由一个字段 seqNum 构成,它是为这个客户端分配的一组序号的起始值。
/*pipes/fifo_seqnum.h
*/#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"#define SERVER_FIFO "/tmp/seqnum_sv"/* Well-known name for server's FIFO */
#define CLIENT_FIFO_TEMPLATE "/tmp/seqnum_cl.%ld"/* Template for building client FIFO name */
#define CLIENT_FIFO_NAME_LEN (sizeof(CLIENT_FIFO_TEMPLATE) + 20)/* Space required for client FIFO pathname(+20 as a generous allowance for the PID) */struct request { /* Request (client --> server) */pid_t pid; /* PID of client */int seqLen; /* Length of desired sequence */
};struct response { /* Response (server --> client) */int seqNum; /* Start of sequence */
};
6.2 服务器程序
服务器按序完成了下面的工作。
- 创建服务器的众所周知的 FIFO并打开 FIFO 以便读取。服务器必须要在客户端之前运行,这样服务器 FIFO 在客户端试图打开它之前就已经存在了。服务器的 open()调用将会阻塞直到第一个客户端打开了服务器的 FIFO 的另一端以写入数据为止。
- 再次打开服务器的 FIFO,这次是为了写入数据。这个调用永远不会被阻塞,因为之前已经因需读取而打开 FIFO 了。第二个打开操作是为了确保服务器在所有客户端关闭了 FIFO 的写入端之后不会看到文件结束。
- 忽略 SIGPIPE 信号,这样如果服务器试图向一个没有读者的客户端 FIFO 写入数据时不会收到 SIGPIPE 信号(默认会杀死进程),而是会从 write()系统调用中收到一个EPIPE 错误。
- 进入一个循环从每个进入的客户端请求中读取数据并响应。要发送响应,服务器需要构建客户端 FIFO 的名称,然后打开这个 FIFO。
- 如果服务器在打开客户端 FIFO 时发生了错误,那么就丢弃那个客户端的请求。
这是一种迭代式服务器,这种服务器会在读取和处理完当前客户端之后才会去处理下一个客户端。当每个客户端请求的处理和响应都能够快速完成时采用这种迭代式服务器设计是合理的,因为不会对其他客户端请求的处理产生延迟。另一种设计方法是并发式服务器,在这种设计中主服务器进程使用单独的子进程(或线程)来处理各个客户端的请求。
/*
pipes/fifo_seqnum_server.c
*/
#include <signal.h>
#include "fifo_seqnum.h"int
main(int argc, char *argv[])
{int serverFd, dummyFd, clientFd;char clientFifo[CLIENT_FIFO_NAME_LEN];struct request req;struct response resp;int seqNum = 0; /* This is our "service" *//* Create well-known FIFO, and open it for reading */umask(0); /* So we get the permissions we want */if (mkfifo(SERVER_FIFO, S_IRUSR | S_IWUSR | S_IWGRP) == -1&& errno != EEXIST)errExit("mkfifo %s", SERVER_FIFO);serverFd = open(SERVER_FIFO, O_RDONLY);if (serverFd == -1)errExit("open %s", SERVER_FIFO);/* Open an extra write descriptor, so that we never see EOF */dummyFd = open(SERVER_FIFO, O_WRONLY);if (dummyFd == -1)errExit("open %s", SERVER_FIFO);/* Let's find out about broken client pipe via failed write() */if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) errExit("signal");for (;;) { /* Read requests and send responses */if (read(serverFd, &req, sizeof(struct request))!= sizeof(struct request)) {fprintf(stderr, "Error reading request; discarding\n");continue; /* Either partial read or error */}/* Open client FIFO (previously created by client) */snprintf(clientFifo, CLIENT_FIFO_NAME_LEN, CLIENT_FIFO_TEMPLATE,(long) req.pid);clientFd = open(clientFifo, O_WRONLY);if (clientFd == -1) { /* Open failed, give up on client */errMsg("open %s", clientFifo);continue;}/* Send response and close FIFO */resp.seqNum = seqNum;if (write(clientFd, &resp, sizeof(struct response))!= sizeof(struct response))fprintf(stderr, "Error writing to FIFO %s\n", clientFifo);if (close(clientFd) == -1)errMsg("close");seqNum += req.seqLen; /* Update our sequence number */}
}
6.3 客户端程序
客户端按序完成了下面的工作。
- 创建一个 FIFO 以从服务器接收响应。这项工作是在发送请求之前完成的,这样才能确保 FIFO 在服务器试图打开它并向其发送响应消息之前就已经存在了。
- 构建一条发给服务器的消息,消息中包含了客户端的进程 ID 和一个指定了客户端希望服务器赋给它的序号长度的数字(从可选的命令行参数中获取)。(如果没有提供命令行参数,那么默认的序号长度是 1。)
- 打开服务器 FIFO并将消息发送给服务器。
- 打开客户端 FIFO,然后读取和打印服务器的响应。
另一个需要注意的地方是通过 atexit()③建立的退出处理器①,它确保了当进程退出之后客户端的 FIFO 会被删除。或者可以在客户端 FIFO 的 open()调用之后立即调用 unlink()。在那个时刻这种做法是能够正常工作的,因为它们都执行了阻塞的 open()调用,服务器和客户端各自持有了 FIFO 的打开着的文件描述符,而从文件系统中删除 FIFO 名称不会对这些描述符以及它们所引用的打开着的文件描述符产生影响。
下面是运行这个客户端和服务器程序时看到的输出:
./fifo_seqnum_server &
[1] 203048
$ ./fifo_seqnum_client 3
0
$ ./fifo_seqnum_client 2
3
$ ./fifo_seqnum_client
5
/*
pipes/fifo_seqnum_client.c
*/
#include "fifo_seqnum.h"static char clientFifo[CLIENT_FIFO_NAME_LEN];static void /* Invoked on exit to delete client FIFO */
removeFifo(void)
{unlink(clientFifo);
}int
main(int argc, char *argv[])
{int serverFd, clientFd;struct request req;struct response resp;if (argc > 1 && strcmp(argv[1], "--help") == 0)usageErr("%s [seq-len]\n", argv[0]);/* Create our FIFO (before sending request, to avoid a race) */umask(0); /* So we get the permissions we want */snprintf(clientFifo, CLIENT_FIFO_NAME_LEN, CLIENT_FIFO_TEMPLATE,(long) getpid());if (mkfifo(clientFifo, S_IRUSR | S_IWUSR | S_IWGRP) == -1&& errno != EEXIST)errExit("mkfifo %s", clientFifo);if (atexit(removeFifo) != 0)errExit("atexit");/* Construct request message, open server FIFO, and send message */req.pid = getpid();req.seqLen = (argc > 1) ? getInt(argv[1], GN_GT_0, "seq-len") : 1;serverFd = open(SERVER_FIFO, O_WRONLY);if (serverFd == -1)errExit("open %s", SERVER_FIFO);if (write(serverFd, &req, sizeof(struct request)) !=sizeof(struct request))fatal("Can't write to server");/* Open our FIFO, read and display response */clientFd = open(clientFifo, O_RDONLY);if (clientFd == -1)errExit("open %s", clientFifo);if (read(clientFd, &resp, sizeof(struct response))!= sizeof(struct response))fatal("Can't read response from server");printf("%d\n", resp.seqNum);exit(EXIT_SUCCESS);
}
6.4 非阻塞I/O
前面曾经提过当一个进程打开一个 FIFO 的一端时,如果 FIFO 的另一端还没有被打开,那么该进程会被阻塞。但有些时候阻塞并不是期望的行为,而这可以通过在调用 open()时指定O_NONBLOCK 标记来实现。
fd =open("fifopath",O_RDONLY | O_NONBLOCK);
if(fd == -1)errExit("open");
如果 FIFO 的另一端已经被打开,那么 O_NONBLOCK 对 open()调用不会产生任何影响——它会像往常一样立即成功地打开 FIFO。只有当 FIFO 的另一端还没有被打开的时候O_NONBLOCK 标记才会起作用,而具体产生的影响则依赖于打开 FIFO 是用于读取还是用于写入的。
- 如果打开 FIFO 是为了读取,并且 FIFO 的写入端当前已经被打开,那么 open()调用会立即成功(就像 FIFO 的另一端已经被打开一样)。
- 如果打开 FIFO 是为了写入,并且还没有打开 FIFO 的另一端来读取数据,那么 open()调用会失败,并将 errno 设置为 ENXIO。
为读取而打开 FIFO 和为写入而打开 FIFO 时 O_NONBLOCK 标记所起的作用不同是有原因的。当 FIFO 的另一个端没有写者时打开一个 FIFO 以便读取数据是没有问题的,因为任何试图从 FIFO 读取数据的操作都不会返回任何数据。但当试图向没有读者的 FIFO 中写入数据时将会导致 SIGPIPE 信号的产生以及 write()返回 EPIPE 错误。
下表对打开 FIFO 的语义进行了总结,包括上面介绍的 O_NONBLOCK 标记的作用。
在打开一个 FIFO 时使用 O_NONBLOCK 标记存在两个目的。
y 它允许单个进程打开一个 FIFO 的两端。这个进程首先会在打开 FIFO 时指定O_NONBLOCK 标记以便读取数据,接着打开 FIFO 以便写入数据。
y 它防止打开两个 FIFO 的进程之间产生死锁。
当两个或多个进程中每个进程都因等待对方完成某个动作而阻塞时会产生死锁。图 44-8给出了两个进程发生死锁的情形。各个进程都因等待打开一个 FIFO 以便读取数据而阻塞。如果各个进策划那个都可以执行其第二个步骤(打开另一个 FIFO 以便写入数据)的话就不会发生阻塞。这个特定的死锁问题是通过颠倒进程 Y 中的步骤 1 和步骤 2 并保持进程 X 中两个步骤的顺序不变来解决,反之亦然。但在一些应用程序中进行这样的调整可能并不容易。相反,可以通过在为读取而打开 FIFO 时让其中一个进程或两个进程都指定 O_NONBLOCK 标记来解决这个问题。
非阻塞 read()和 write()
O_NONBLOCK 标记不仅会影响 open()的语义,而且还会影响——因为在打开的文件描述中这个标记仍然被设置着——后续的 read()和 write()调用的语义。下一节将会对这些影响进行描述。
有些时候需要修改一个已经打开的 FIFO(或另一种类型的文件)的 O_NONBLOCK 标记的状态,具体存在这个需求的场景包括以下几种。
y 使用 O_NONBLOCK 打开了一个 FIFO 但需要让后续的 read()和 write()调用在阻塞模式下运作。
y 需要启用从 pipe()返回的一个文件描述符的非阻塞模式。更一般地,可能需要更改从除open()调用之外的其他调用中——如每个由shell运行的新程序中自动被打开的三个标准描述符的其中一个或 socket()返回的文件描述符——取得的任意文件描述符的非阻塞状态。
y 出于一些应用程序的特殊需求,需要切换一个文件描述符的 O_NONBLOCK 设置的开启和关闭状态。
当碰到上面的需求时可以使用 fcntl()启用或禁用打开着的文件的 O_NONBLOCK 状态标记。通过下面的代码(忽略的错误检查)可以启用这个标记。
int flags;
flags =fcntl(fd,F_GETFL); /*Fetch open files status flags */
flagS |= O_NONBLOCK; /* Enable O NONBLOCK bit */
fcntl(fd,F_SETFL, flags); /*Update open files status flags */
通过下面的代码可以禁用这个标记。
flags = fcntl(fd,F_GETFL);
flags &= ~O_NONBLOCK; /* Disable 0 NONBLOCK bit */
fcntl(fd,F_SETFL,flags);
7 管道和 FIFO 中 read()和 write()的语义
表 44-2 对管道和 FIFO 上的 read()操作进行了总结,包括 O_NONBLOC 标记的作用。
只有当没有数据并且写入端没有被打开时阻塞和非阻塞读取之间才存在差别。在这种情况下,普通的 read()会被阻塞,而非阻塞 read()会失败并返回 EAGAIN 错误。
当 O_NONBLOCK 标记与 PIPE_BUF 限制共同起作用时 O_NONBLOCK 标记对象管道或FIFO 写入数据的影响会变得复杂。表 44-3 对 write()的行为进行了总结。
当数据无法立即被传输时 O_NONBLOCK 标记会导致在一个管道或 FIFO 上的 write()失败(错误是 EAGAIN)。这意味着当写入了 PIPE_BUF 字节之后,如果在管道或 FIFO 中没有足够的空间了,那么 write()会失败,因为内核无法立即完成这个操作并且无法执行部分写入,否则就会破坏不超过 PIPE_BUF 字节的写入操作的原子性的要求。
当一次写入的数据量超过 PIPE_BUF 字节时,该写入操作无需是原子的。因此,write()会尽可能多地传输字节(部分写)以充满管道或 FIFO。在这种情况下,从 write()返回的值是实际传输的字节数,并且调用者随后必须要进行重试以写入剩余的字节。但如果管道或 FIFO已经满了,从而导致哪怕连一个字节都无法传输了,那么 write()会失败并返回 EAGAIN 错误。
相关文章:
Linux/AndroidOS中进程间的通信线程间的同步 - 管道和FIFO
前言 管道是 UNIX 系统上最古老的 IPC 方法,它在 20 世纪 70 年代早期 UNIX 的第三个版本上就出现了。管道为一个常见需求提供了一个优雅的解决方案:给定两个运行不同程序的进程,在 shell 中如何让一个进程的输出作为另一个进程的输入呢&…...
第十六届蓝桥杯 2025 C/C++组 破解信息
目录 题目: 题目描述: 题目链接: 思路: 思路详解: 代码: 代码详解: 题目: 题目描述: 题目链接: P12344 [蓝桥杯 2025 省 B/Python B 第二场] 破解信息…...
[FPGA Video IP] VDMA
Xilinx AXI Video Direct Memory Access IP (PG020) 详细介绍 概述 Xilinx AXI Video Direct Memory Access (AXI VDMA) LogiCORE™ IP 核(PG020)是一个软核 IP,专为视频应用设计,提供在内存与 AXI4-Stream 视频外设之间的高带宽…...
Ubuntu如何查看硬盘的使用情况,以及挂载情况。
在Ubuntu中查看硬盘使用情况及挂载情况,可通过以下命令实现: 一、查看硬盘使用情况 df -h 显示所有挂载文件系统的磁盘空间使用情况(含总容量、已用空间、可用空间等),输出结果以易读格式(如GB、MB&#x…...
第十六届蓝桥杯 2025 C/C++组 脉冲强度之和
目录 题目: 题目描述: 题目链接: 思路: 思路详解: 代码: 代码详解: 题目: 题目描述: 题目链接: P12338 [蓝桥杯 2025 省 B/Python B 第二场] 脉冲强度…...
23种设计模式-行为型模式之中介者模式(Java版本)
Java 中介者模式(Mediator Pattern)详解 🧠 什么是中介者模式? 中介者模式是一种行为型设计模式,它通过定义一个中介者对象来封装一组对象之间的交互。中介者使得各个对象不需要显式地知道彼此之间的关系,…...
集群与存储-lvs-nat实验
一、实验目的 1、熟练掌握ipvsadm 指令的使用 2、理解lvs 负载均衡的原理 3. 熟练掌握lvs-netnfs的配置 二、实验内容 1.拓扑图 2.实验步骤 #在节点服务器中添加网卡,选择仅主机模式 #查看网卡 #启用网卡 nmcli device up ens224 ip a #修改虚拟地址的网关 #…...
Android——Serializable和Parcelable
在Android中传递对象的方式 在 Android 开发中,Parcelable 和 Serializable 是两种用于对象序列化的接口 Serializable public class Student implements Serializable {public int id;public String name;public int age;public Student(int id, String name, i…...
python:sklearn 决策树(Decision Tree)
5. 决策树(Decision Tree) - 第5章 算法思想:基于信息增益(ID3)或基尼不纯度(CART)递归划分特征。 编写 test_dtree_1.py 如下 # -*- coding: utf-8 -*- """ 5. 决策树&…...
0-1背包难题哪家强:回溯法 VS 动态规划 VS 贪心算法
回溯法、动态规划和贪心算法是三种常见的算法设计思想,他们都可以用来解决0-1背包问题,但它们在解决问题的思路、适用条件和效率上存在显著差异。以下从多个维度进行对比分析: 相关系列文章链接: 《贪心算法 vs 动态规划:“急性子…...
JavaSE第12篇:接口interface
一、使用步骤 1.引入库 代码如下(示例): import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import warnings warnings.filterwarnings(ignore) import ssl ssl._create_default_https_con…...
一文掌握 npm 基础与常用指令
初学前端?npm 常用指令不熟?想了解 pnpm、yarn、cnpm 有什么不同? 这篇文章将带你从入门到精通,全面掌握 npm 的使用方法,以及选择适合自己项目的包管理工具! 文章目录 一、什么是 npm?二、npm …...
OpenObserve API Usage Guide for Log Management
OpenObserve API Usage Guide for Audit Log Management 1. 概述 1.1 目标 本文档旨在详细介绍 OpenObserve 的 API 使用方法,帮助用户通过 API 实现日志管理功能,包括日志摄入、查询、模糊匹配(类似 SQL 的 LIKE)、stream 管理…...
机器学习实操 第一部分 机器学习基础 第5章 支持向量机(SVM)
机器学习实操 第一部分 机器学习基础 第5章 支持向量机(SVM) 内容概要 第5章深入介绍了支持向量机(SVM),这是一种功能强大且应用广泛的机器学习模型。SVM适用于线性或非线性分类、回归以及 novelty detection。本章详…...
CSRF(cross-site request forgery)跨域请求访问
CSRF 当我们在成功登录一个网站后,会将后端返回的cookie数据进行存放,每一次访问该域名都会将cookie存放在请求头,也就相当于用户登录凭证, 但这种同域自动携带cookie存在一种问题 那就是当恶意网站也进去请求时,同样…...
Kafka的Rebalance机制可能引发什么问题?如何优化?怎么减少不必要的Rebalance
Rebalance机制的核心目的是确保每个消费者都能处理适当数量的分区,以实现负载均衡和高可用性。 一般是消费者组发生变化的时候,比如订阅主题,消费者数量等等发生变化,可能会导致rebalance,rebalance会导致消费者组短时…...
【和春笋一起学C++】函数——C++的编程模块
目录 1. 原型句法 2. 函数分类 3. 函数参数之按值传递 4. 数组作为函数参数 在C中,要使用函数,必须要有这三个方面: 函数原型,函数原型描述了函数到编译器的接口,函数原型一般放在include文件中。函数原型告诉编译…...
Java高频面试之并发编程-11
hello啊,各位观众姥爷们!!!本baby今天又来报道了!哈哈哈哈哈嗝🐶 面试官:父子线程如何共享数据? 在Java中,父子线程共享数据可以通过以下几种方式实现,具体…...
LangChain入门(四) 部署应用程序
1、使用LangServe部署应用程序 安装langserve pip install langserve[all] 代码示例 from fastapi import FastAPI from langchain.chat_models import init_chat_model from langchain_core.messages import SystemMessage, HumanMessage from langchain_core.output_parser…...
精益数据分析(31/126):电商关键指标深度解析与实战策略
精益数据分析(31/126):电商关键指标深度解析与实战策略 在创业和数据分析的探索之路上,每一次深入学习都像是解锁了新的技能,让我们离成功更近一步。今天,我依旧带着和大家共同进步的想法,深入…...
【MongoDB篇】MongoDB的集合操作!
目录 引言第一节:集合的“诞生”——自动出现还是手动打造?🤔第二节:集合的“查阅”——看看这个数据库里有哪些柜子?📂👀第三节:集合的“重命名”——给文件柜换个名字!…...
antd中的表格穿梭框(Transfer)如何使用
穿梭框是什么?怎么使用? 需求如下: 有一组端口需要分配给具体接口 功能要求: 1. 需要展示当前端口名称及其所属的接口 2. 需支持搜索功能可对端口名或接口名进行筛选便于分配 3. 分配端口时,需检测当前接口内的端口是否满足此接口最低要求 4. 提供Select下拉框,可供查…...
联邦学习与安全多方计算的结合是隐私保护机器学习领域
联邦学习(Federated Learning, FL)与安全多方计算(Secure Multi-Party Computation, MPC)的结合是隐私保护机器学习领域的前沿方向,其框架设计需兼顾计算效率、安全性和可扩展性。以下是结合两者的框架设计与实现流程的详细解析: 一、框架设计核心目标 隐私保护:确保多…...
mongoose的介绍,连接数据库
Mongoose 是一个基于 Node.js 的 MongoDB ODM(Object Data Modeling)库,用于在 MongoDB 和 Node.js 应用之间提供结构化的模型层,帮助你更优雅、安全地操作数据库。 🧾 一、Mongoose 简介 📦 功能ÿ…...
Pytest中的fixture装饰器详解
pytest是Python生态中最流行的自动化测试框架,它通过简洁的语法、强大的功能(如fixture、参数化、插件扩展等)和丰富的插件生态,帮助开发者高效完成单元测试、集成测试和端到端测试。fixture是pytest框架中最核心、最强大的功能之一,它提供了…...
Linux系统配置JDK
目录 一、xftp传输JDK包 1、新建xftp会话并连接到我们的服务器 2、上传jdk包 二、配置环境变量 为了方便javaweb项目的建立,我们需要在搭建好的linux环境下配置安装JDK环境 一、xftp传输JDK包 因为jdk包文件比较大了,这时候不能使用简单的linux上传…...
通义千问最新一代大语言模型Qwen3发布了
通义千问Qwen3全面解析:最强开源大模型Ollama本地运行实战 🔥 最新重大好消息! 经过漫长的等待,今天凌晨阿里云正式发布了Qwen3大语言模型!本次更新带来了0.6b 1.7b 4b 8b 14b 30b 32b 235b超大参数模型,更…...
想做博闻强记的自己
2025年4月29日,13~25℃,还好 待办: 冶金《物理》期末测试 阅卷(冶金《物理》期末测试试卷) 重修《物理》《物理2》电子材料归档 规则变更,《高等数学2》期末试卷推倒重来 遇见:直播画面。 感受…...
爱普生SG2520HHN晶振数据中心服务器的理想解决方案
在当今数字化时代,数据中心作为海量数据存储、处理与传输的核心枢纽,其服务器的高效稳定运行至关重要。服务器作为其核心设备,对时钟信号的精度和稳定性提出了严苛要求——微小的时序误差可能导致数据传输失败或系统宕机。爱普生 SG2520HHN 差…...
【Prometheus-MySQL Exporter安装配置指南,开机自启】
目录 1. 创建 MySQL 监控用户2. 配置 MySQL 认证文件3. 安装 mysqld_exporter4. 配置 Systemd 服务5. 启动并验证服务6. 修改Prometheus配置常见错误排查错误现象排查步骤 6. 验证监控数据关键注意事项 1. 创建 MySQL 监控用户 mysql -uroot -p123456 # 登录MySQL-- 1. 创建监…...
Linux 服务管理两种方式service和systemctl
Linux 服务管理两种方式service和systemctl 确定当前系统使用的哪种命令用来启动服务 SysV init 或者 systemd 使用下面的命令: ps -p 1例如,输出: PID TTY TIME CMD1 ? 00:00:02 systemdSysV init service命令用于对系统…...
P1494 [国家集训队] 小 Z 的袜子 Solution
Description 给定序列 a ( a 1 , a 2 , ⋯ , a n ) a(a_1,a_2,\cdots,a_n) a(a1,a2,⋯,an),有 q q q 次查询,每次查询给定 ( l , r ) (l,r) (l,r). 你需要求出 2 ∑ i ≤ i < j ≤ r [ a i a j ] ( r − l ) ( r − l 1 ) \dfrac{2\sum…...
(开源)视频画面增强模型:Ev-DeblurVSR (可以解决视频画面不清晰的问题)
在计算机视觉领域,模糊视频超分辨率(BVSR)是一个复杂且具有挑战性的任务,目标是从低分辨率(LR)和模糊的输入生成高分辨率(HR)视频。传统方法常常因缺乏足够运动信息和高频细节而表现…...
探索豆包WEB/PC超能创意1.0:创意新利器的全面解析
在当今数字化创意蓬勃发展的时代,新工具不断涌现,为创作者们带来了更多的可能性。豆包WEB/PC超能创意1.0便是其中一款备受瞩目的产品,它的出现为创意工作者和爱好者们打开了一扇充满无限可能的大门。 一、体验信息:探索创意新领域…...
五、UI自动化测试05--PyTest框架
目录 一、PyTest 框架2. 特点2. 安装步骤3. 基本使⽤3.1 测试函数形式3.2 执⾏⽅式3.3 测试类形式3.4 执⾏⽅式3.5 另⼀种执⾏⽅式: 主函数执⾏3.6 特殊⽅法: 函数级别3.7 特殊⽅法: 类级别3.8 特殊⽅法: 函数级别和类级别同时使⽤ 4. pytest 配置⽂件4.1 选项字段获取4.2 编写…...
51LA使用方法与悟空统计,网站数据分析的双重选择
在网站运营与数据分析领域,51LA作为国内较早的流量统计工具,曾为许多用户提供基础的访问数据监测服务。然而,随着技术的发展和用户需求的升级,越来越多的企业开始寻求功能更全面、体验更优的统计工具。小编今天将给大家介绍一款更…...
MongoDB的下载安装与启动
MongoDB的下载安装与启动, 一、MongoDB下载安装 1. 官网下载 打开官网:https://www.mongodb.com/try/download/community选择: 版本(Version):选最新版或者根据需要选旧版。平台(OS࿰…...
解决ktransformers v0.3 docker镜像中 operator torchvision::nms does not exist 问题
问题背景 更新ktransformers docker镜像到v0.3版本后(之前为v0.2.4post1),使用更新前启动命令无法正确启动服务,提示以下错误: Traceback (most recent call last):File "/workspace/ktransformers/ktransforme…...
MySQL事务隔离级别的实现原理MVCC
一、什么是MVCC? MVCC(Multi-Version Concurrency Control),即多版本并发控制,是并发读写场景下,数据库层面提供的一种解决方案。 数据库的并发场景有以下三种: 读读 当多个事务同时进行读取操作时,它们之间不存在…...
EtherCAT 分布式时钟(DC)补偿技术解析
一、技术定义 EtherCAT 分布式时钟(Distributed Clock, DC)是一种基于硬件的高精度同步机制,旨在解决工业自动化系统中多设备协同控制的时间同步问题。其核心功能包括: 初始偏移补偿:消除从站本地时钟与主站系统时间的初始偏差,确保所有设备在启动阶段的时间基准一致。…...
7.进程概念(三)
一、进程优先级 是什么? 进程得到CPU资源的先后顺序。 为什么要有进程优先级? 目标资源稀缺,导致要通过优先级确定谁先谁后。 如何比较和分配? 进程优先级也是一种数字,int,task_struct 值越低,…...
MATLAB小试牛刀系列(2)
问题描述 捷运公司在下一年度 1 - 4 月的 4 个月内拟租用仓库堆放物资。已知各月所需仓库面积列于表 1.1。仓库租借费用随合同期而定,期限越长,折扣越大,具体数字见表 1.1。租借合同每月初都可办理,每份合同具体规定租用面积和期…...
一个SciPy图像处理案例的全过程
本文利用SciPy进行图像处理,并记录图像处理的全过程,处理过程包含高斯模糊、腐蚀等操作。 代码 import matplotlib.pyplot as plt import numpy as np from scipy import ndimage# 设置图像的大小为 128x128,即 128x128 的逻辑像素 l 128 …...
修改输入框选择框颜色
项目场景: 提示:这里简述项目相关背景: 有时候需要改写element原来输入框/选择框的颜色 问题描述 提示:这里描述项目中遇到的问题: 输入框的话需要hover时边框颜色修改,选择值的时候边框颜色修改以及选…...
rust 全栈应用框架dioxus
逛github时发现了一个号称全栈应用框架dioxus,适用于web / desktop / mobile。零配置、集成了热启动和基于信号的状态管理。是由rust编写的,所以也就不受平台限制。 既然说的这么好,那就来试试构建一下三种平台的应用,构建的应用编译成web 、…...
电子电器框架 --- 数据连接性和云集成在增强电气/电子架构方面的作用
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…...
Nvidia 可能会发布具有增强内存配置的 RTX 5080 和 5070 Super
距离英伟达正式发布RTX 50系列显卡仅过去数月,有关"Super"系列升级版显卡的传闻已甚嚣尘上。据硬件爆料平台Chiphell论坛(该消息源可靠性参差不齐)用户透露,英伟达可能正在研发配备24GB显存的RTX 5080 Super和16GB显存的…...
预留库存的实现
1. 实体类 import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data;import java.sql.Timestamp;Data TableName("products") public class Product {private Long id;private String name;private int stock; }Data TableName("shopping_c…...
[逆向工程]如何理解小端序?逆向工程中的字节序陷阱与实战解析
[逆向工程]如何理解小端序?逆向工程中的字节序陷阱与实战解析 关键词:逆向工程、小端序、字节序、二进制分析、数据解析 引言:为什么字节序是逆向工程师的必修课? 在逆向工程中,分析二进制数据是最基础的任务之一。…...
【Python笔记 05】 if判断、比较运算符与逻辑运算符
一、if判断 1、基本格式 if 要判断的条件: #条件成立为true条件成立的时候要做的事情注:注意判断条件后面的冒号,以及条件成立要做的事情此行代码的缩进,最好是软件自动缩进。 2、练习题 用户在控制台输入成绩,…...