linux进程间通讯指南-打通IPC大门,高效沟通无阻


在现代操作系统中,进程就像独立的个体,有时需要相互合作、数据共享,这就要求进程间能够高效通信。本文将为你揭开Linux进程间通信(IPC)的神秘面纱,探讨各种IPC工具的运作原理,同步机制的重要性,以及如何规避潜在风险。我们将通过丰富的C++示例,让你融会贯通IPC实践。


一、IPC概述:进程间畅所欲言


所谓IPC(Inter-Process Communication),就是指允许进程之间传递数据或进行通信控制的机制。在Linux下,主要的IPC工具包括管道(Pipe)、FIFO、消息队列(Message Queue)、共享内存(Shared Memory)、信号(Signal)等。


IPC工具可以分为两大类:

  • 数据传输工具:如管道、消息队列等,用于在进程间传递数据。

在这里插入图片描述


  • 同步工具:信号量、文件锁等同步工具,则控制对共享资源的访问顺序,避免竞争条件。

    在这里插入图片描述


二、选择恰当的IPC工具


1、数据传输工具

从上面的分类中我们可以看到,IPC 工具有很多,而区分这些工具的关键因素就是数据读取和写入的形式。

比如说,一些
IPC 工具要求在写数据时将数据从用户内存传输至内核内存,读取数据时则将数据从内核内存输入到用户内存。

其中最典型的就是流 式
socket 和管道。


(1)、流 式
socket

流式
socket 数据必须从用户缓冲区写入至
TCP 连接的发送缓冲区中 ,读取数据时则从
TCP 连接的接收缓冲区进行读取。

流式socket通常指的是面向连接的TCP socket,数据的发送和接收通过socket描述符来进行。

以下是一个简单的示例,演示如何使用TCP socket进行数据的发送和接收:

#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    const int server_port = 8080;
    const char* server_ip = "127.0.0.1";
    int sock_fd;
    struct sockaddr_in server_addr;

    // 创建socket
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        std::cerr << "Failed to create socket" << std::endl;
        return 1;
    }

    // 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);
    if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) <= 0) {
        std::cerr << "Invalid address" << std::endl;
        close(sock_fd);
        return 1;
    }

    // 连接到服务器
    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        std::cerr << "Failed to connect to the server" << std::endl;
        close(sock_fd);
        return 1;
    }

    // 发送数据
    const char* message = "Hello, Server!";
    if (send(sock_fd, message, strlen(message), 0) < 0) {
        std::cerr << "Send failed" << std::endl;
        close(sock_fd);
        return 1;
    }
    std::cout << "Message sent" << std::endl;

    // 接收数据
    char buffer[1024] = {0};
    ssize_t bytes_received = recv(sock_fd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_received < 0) {
        std::cerr << "Receive failed" << std::endl;
        close(sock_fd);
        return 1;
    }
    std::cout << "Message received: " << buffer << std::endl;

    // 关闭socket
    close(sock_fd);
    return 0;
}

在这个示例中,我们首先创建了一个TCP socket,然后设置了服务器的地址和端口,并尝试连接到服务器。一旦连接成功,我们使用send函数将一个字符串消息发送到服务器,消息数据从用户缓冲区(在这个例子中是message字符串)写入到TCP连接的发送缓冲区中。


接收数据时,我们使用recv函数从TCP连接的接收缓冲区读取数据到用户缓冲区(buffer数组)。如果recv函数成功,它返回接收到的字节数,我们将其打印出来。


最后,我们使用close函数关闭socket。

请注意,这个示例是一个阻塞的socket操作,sendrecv函数在数据发送或接收完成之前会阻塞。在实际的网络编程中,你可能需要处理更多的错误情况,并可能需要使用非阻塞socket或信号驱动I/O等技术来提高程序的响应性和性能。


(2)、管道

在Linux中,管道(pipe)是一种进程间通信(IPC)机制,允许一个进程(生产者)与另一个进程(消费者)通过一个缓冲区交换数据。管道是单向的,数据只能在一个方向上流动,并且通常用于父子进程或者兄弟进程之间的通信。

在这里插入图片描述


以下是演示管道通信的基本流程:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <cerrno>
#include <cstring>

int main() {
    int pipefds[2];
    pid_t pid;
    const char *message = "Hello, this is the message going through the pipe!";
    char readbuffer[128];

    // 创建管道
    if (pipe(pipefds) == -1) {
        std::cerr << "Pipe failed: " << strerror(errno) << std::endl;
        return 1;
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        std::cerr << "Fork failed" << std::endl;
        close(pipefds[0]); // 读取端
        close(pipefds[1]); // 写入端
        return 1;
    }

    if (pid > 0) {
        // 父进程,关闭读取端,使用写入端
        close(pipefds[0]);

        // 写数据到管道
        if (write(pipefds[1], message, strlen(message)) == -1) {
            std::cerr << "Write to pipe failed: " << strerror(errno) << std::endl;
        }
        close(pipefds[1]); // 关闭写入端

        // 等待子进程结束
        wait(NULL);
    } else {
        // 子进程,关闭写入端,使用读取端
        close(pipefds[1]);

        // 从管道读取数据
        if (read(pipefds[0], readbuffer, sizeof(readbuffer)) == -1) {
            std::cerr << "Read from pipe failed: " << strerror(errno) << std::endl;
        }
        readbuffer[strcspn(readbuffer, "\n")] = 0; // 去除换行符
        std::cout << "Message received through pipe: " << readbuffer << std::endl;

        close(pipefds[0]); // 关闭读取端
    }

    return 0;
}

在这个示例中,我们首先使用pipe系统调用创建了一个管道,并获取了两个文件描述符:pipefds[0](用于读取)和pipefds[1](用于写入)。


然后,我们使用fork创建了一个子进程。

在父进程中,我们关闭了读取端(pipefds[0]),并通过写入端发送了一个字符串消息到管道。

在子进程中,我们关闭了写入端(pipefds[1]),并从读取端读取了管道中的数据。

请注意,管道的缓冲区大小通常是有限的,Linux中默认的管道缓冲区大小为65536字节。如果生产者写入的数据超过了缓冲区的大小,写入操作将会阻塞,直到消费者读取了足够的数据,释放了缓冲区空间。


此外,由于管道是单向的,所以通常需要两个管道来进行双向通信:一个用于从父进程到子进程的通信,另一个用于从子进程到父进程的通信。而且,管道是半双工的,意味着在任何给定时间,只能进行一个方向的通信。


(3)、FIFO


FIFO(也称为命名管道或具名管道)提供了一种在不相关的进程之间进行通信的方式,与管道类似,但具有文件系统中的名称。这意味着任何进程都可以通过FIFO的路径名来打开它,进行读写操作。

以下演示FIFO通信的基本流程:

#include <iostream>
#include <fcntl.h>    // 包含 open 函数
#include <unistd.h>   // 包含 read, write 函数
#include <sys/stat.h>// 包含 S_IRUSR, S_IWUSR, S_IRGRP, S_IWGRP, S_IROTH, S_IWOTH
#include <cstring>

// 创建FIFO
bool create_fifo(const char* fifo_name) {
    // 使用mkfifo创建FIFO
    if (mkfifo(fifo_name, 0666) == -1) { // 0666 表示读写权限给所有用户
        std::cerr << "Failed to create FIFO: " << strerror(errno) << std::endl;
        return false;
    }
    return true;
}

// 写入FIFO
bool write_fifo(const char* fifo_name, const char* message) {
    int fd = open(fifo_name, O_WRONLY);
    if (fd == -1) {
        std::cerr << "Failed to open FIFO for writing: " << strerror(errno) << std::endl;
        return false;
    }

    if (write(fd, message, strlen(message)) == -1) {
        std::cerr << "Failed to write to FIFO: " << strerror(errno) << std::endl;
        close(fd);
        return false;
    }

    close(fd);
    return true;
}

// 从FIFO读取
bool read_fifo(const char* fifo_name) {
    int fd = open(fifo_name, O_RDONLY);
    if (fd == -1) {
        std::cerr << "Failed to open FIFO for reading: " << strerror(errno) << std::endl;
        return false;
    }

    char buffer[1024];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
    if (bytes_read == -1) {
        std::cerr << "Failed to read from FIFO: " << strerror(errno) << std::endl;
        close(fd);
        return false;
    }
    buffer[bytes_read] = '\0'; // 确保字符串以null结尾

    std::cout << "Message received from FIFO: " << buffer << std::endl;
    close(fd);
    return true;
}

int main() {
    const char* fifo_name = "/tmp/my_fifo";

    // 创建FIFO
    if (!create_fifo(fifo_name)) {
        return 1;
    }

    // 写入FIFO
    if (!write_fifo(fifo_name, "Hello, this is the message going through the FIFO!")) {
        return 1;
    }

    // 读取FIFO
    if (!read_fifo(fifo_name)) {
        return 1;
    }

    // 可以选择删除FIFO
    // unlink(fifo_name);

    return 0;
}

在这个示例中,我们首先定义了三个函数:

  • create_fifo:创建一个FIFO。
  • write_fifo:向FIFO写入数据。
  • read_fifo:从FIFO读取数据。

main函数中,我们首先创建了一个FIFO,然后向其写入了一条消息,最后从FIFO中读取消息并打印出来。

请注意,FIFO是一个阻塞设备,如果打开FIFO进行读取但没有进程写入数据,读取操作将阻塞。同样,如果打开FIFO进行写入但没有进程读取数据,写入操作也将阻塞。为了处理这种情况,通常需要使用非阻塞打开或配合使用信号和多路复用技术(如selectpoll)。

此外,FIFO的权限可以通过mkfifo的第二个参数来设置,类似于文件的权限设置。在上面的示例中,我们使用了0666,这表示FIFO对所有用户都是可读可写的。在实际应用中,应根据需要设置适当的权限。


(4)、其他数据传输工具-消息队列和udp

消息队列和UDP(用户数据报协议)是两种不同的进程间通信(IPC)和网络通信机制。它们都通过将数据写入内核来进行操作,但是它们在数据传输的方式和特性上有所不同。

  • 消息队列

消息队列是UNIX系统提供的一种IPC机制,允许进程发送和接收消息。消息队列通过key来标识,进程可以通过这个key来发送和接收消息。


以下是消息队列的演示:

#include <iostream>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <cstring>

// 消息结构体
struct msgbuf {
    long mtype;
    char mtext[256];
};

int main() {
    key_t key = ftok("some_file", 'a');  // 创建唯一的key
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    int msgid = msgget(key, 0666 | IPC_CREAT);  // 创建消息队列
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }

    // 初始化消息
    msgbuf msg;
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello, this is a message in the queue!");

    // 发送消息
    if (msgsnd(msgid, &msg, sizeof(msg.mtext), 0) == -1) {
        perror("msgsnd");
        msgctl(msgid, IPC_RMID, NULL);
        return 1;
    }

    // 接收消息
    msgrcv(msgid, &msg, sizeof(msg.mtext), msg.mtype, 0);
    std::cout << "Received message: " << msg.mtext << std::endl;

    // 删除消息队列
    if (msgctl(msgid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        return 1;
    }

    return 0;
}

在上面的示例中,我们首先使用ftok函数创建一个唯一的key,然后使用msgget函数创建或获取一个消息队列。我们定义了一个msgbuf结构体来存储消息的类型和内容。然后,我们使用msgsnd函数发送消息,使用msgrcv函数接收消息。最后,我们使用msgctl函数删除消息队列。


  • UDP套接字

UDP是一种无连接的网络通信协议,它允许应用程序发送和接收数据报(datagrams)。UDP不保证数据报的顺序、完整性或可靠性,因此它通常用于那些可以容忍一定丢包率的应用,如视频会议或在线游戏。


以下代码演示UDP通信:

#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    const char *message = "Hello, this is a UDP datagram!";
    char buffer[1024];

    // 创建UDP socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return 1;
    }

    // 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 发送数据
    if (sendto(sockfd, message, strlen(message), 0,
               (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("sendto");
        close(sockfd);
        return 1;
    }
    std::cout << "UDP datagram sent" << std::endl;

    // 接收数据
    socklen_t len = sizeof(client_addr);
    ssize_t bytes_received = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                                      (struct sockaddr*)&client_addr, &len);
    if (bytes_received < 0) {
        perror("recvfrom");
        close(sockfd);
        return 1;
    }
    buffer[bytes_received] = '\0'; // 确保字符串以null结尾
    std::cout << "UDP datagram received: " << buffer << std::endl;

    // 关闭socket
    close(sockfd);
    return 0;
}

在这个示例中,我们首先使用socket函数创建了一个UDP socket。然后,我们设置了服务器的地址和端口,并使用sendto函数发送了一个数据报。我们使用recvfrom函数接收了数据报,并打印了接收到的数据。最后,我们关闭了socket。

请注意,UDP是面向数据报的,每次发送和接收操作都是独立的,没有顺序或连接的概念。因此,每次写入和读取都是完整的一条消息,不能使用字节流的方式进行写入。


2、共享内存

共享内存是一种高效的进程间通信(IPC)机制,它允许两个或多个进程共享一个给定的存储区。由于共享内存允许进程直接访问同一块内存,因此它比管道、消息队列或套接字等其他IPC机制具有更高的性能。但是,共享内存需要适当的同步机制来避免竞态条件和数据不一致的问题。


以下是一个简单的示例,演示了如何使用共享内存和信号量来实现进程间的同步:

#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <cstring>

int main() {
    key_t key = ftok("some_file", 'a');
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    // 创建共享内存段
    int shm_id = shmget(key, sizeof(int), IPC_CREAT | 0666);
    if (shm_id < 0) {
        perror("shmget");
        return 1;
    }

    // 附加共享内存
    int *shared_mem = (int *)shmat(shm_id, NULL, 0);
    if (shared_mem == (void *)-1) {
        perror("shmat");
        return 1;
    }

    // 创建信号量
    int sem_id = semget(key, 1, IPC_CREAT | 0666);
    if (sem_id < 0) {
        perror("semget");
        return 1;
    }

    // 设置信号量的值
    if (semctl(sem_id, 0, SETVAL, 1) == -1) { // 初始值设为1
        perror("semctl SETVAL");
        return 1;
    }

    // 写入共享内存的进程
    *shared_mem = 42; // 写入数据

    // 等待信号量
    struct sembuf p_op = {sem_id, 0, -1};
    semop(sem_id, &p_op, 1);

    // 执行其他任务...

    // 通知信号量
    struct sembuf v_op = {sem_id, 0, 1};
    semop(sem_id, &v_op, 1);

    // 从共享内存分离
    if (shmdt(shared_mem) == -1) {
        perror("shmdt");
        return 1;
    }

    // 删除共享内存和信号量
    if (shmctl(shm_id, IPC_RMID, NULL) == -1) {
        perror("shmctl IPC_RMID");
        return 1;
    }
    if (semctl(sem_id, 0, IPC_RMID) == -1) {
        perror("semctl IPC_RMID");
        return 1;
    }

    return 0;
}

在这个示例中,我们首先使用ftok函数创建一个唯一的key,然后使用shmget函数创建一个共享内存段。我们使用shmat函数将共享内存附加到当前进程的地址空间,并将其映射到一个int指针上。

接着,我们使用semget函数创建一个信号量集,并使用semctl函数将其初始值设置为1。我们定义了两个信号量操作:p_op用于等待(P操作),v_op用于通知(V操作)。

在写入共享内存之前,我们执行了P操作,这会等待信号量的值为正。写入完成后,我们执行了V操作,这会增加信号量的值,允许其他进程访问共享内存。

最后,我们使用shmdt函数从共享内存分离,使用shmctlsemctl函数分别删除共享内存段和信号量集。

请注意,这个示例仅演示了单个进程中共享内存和信号量的使用。在实际的多进程环境中,你需要创建多个进程,并在它们之间同步对共享内存的访问。通常,这是通过在父进程中创建共享内存和信号量,然后在子进程中附加共享内存和操作信号量来实现的。


三、同步机制:规避惊心动魄


1、信号量

信号量(Semaphore)是一种同步机制,用于控制多个进程或线程对共享资源的访问。本质上就是内核维护的一个整数,其值永远不会小于
0。如果 一个进程试图将信号量的值减少至小于
0,那么内核会阻塞该操作,直到信号量增长到允许执行该操作的程度。


通常我们会使用一个二元信号量,也就是信号量的值要么是
0,要么是
1。此时非常类似于
mutex,mutex 的状态要么是已上锁,要么是未上锁 。


在C++中,可以使用 POSIX 线程库(pthread)提供的信号量功能。以下是使用二元信号量(也称为互斥锁,mutex)的一个简单示例:

#include <iostream>
#include <pthread.h>
#include <semaphore.h>

// 创建一个信号量对象
sem_t sem;

// 线程函数,尝试对共享资源进行操作
void* thread_function(void* arg) {
    // 等待(P操作)信号量,直到信号量的值大于0
    sem_wait(&sem);

    // 临界区开始
    std::cout << "Thread " << std::this_thread::get_id()
              << " is in the critical section." << std::endl;

    // ... 执行对共享资源的操作 ...

    // 临界区结束
    // 通知(V操作)信号量,增加其值
    sem_post(&sem);

    return nullptr;
}

int main() {
    // 初始化信号量,设置其值为1
    sem_init(&sem, 0, 1);

    // 创建线程
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread_function, NULL);
    pthread_create(&t2, NULL, thread_function, NULL);

    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    // 销毁信号量
    sem_destroy(&sem);

    return 0;
}

在这个示例中,我们首先使用sem_init函数初始化了一个信号量sem,其初始值为1。然后创建了两个线程t1t2,它们都尝试执行thread_function函数。

thread_function函数中,我们首先调用sem_wait函数来执行P操作,这会阻塞线程直到信号量的值大于0。一旦信号量的值大于0,sem_wait函数会减少信号量的值,然后线程可以进入临界区。在临界区内,线程可以安全地访问共享资源。

当线程完成对共享资源的访问后,它会调用sem_post函数来执行V操作,这会增加信号量的值,允许其他等待的线程进入临界区。

最后,在main函数中,我们等待所有线程结束,然后使用sem_destroy函数销毁信号量。

请注意,这个示例演示了如何使用信号量来同步两个线程对共享资源的访问。在实际应用中,信号量可以用于更复杂的同步场景,包括跨进程同步。此外,使用信号量时需要小心,以避免死锁和其他同步问题。


2、文件锁

文件锁是一种用于进程间同步的机制,它通过锁定文件的特定部分来实现。在UNIX和类UNIX系统中,文件锁通常通过fcntl函数来管理。

文件锁分为两种类型:

  • 读锁(共享锁):允许多个进程读取文件,但不能写入。

  • 写锁(互斥锁):只允许一个进程写入文件,其他进程不能读取也不能写入。

文件锁是自动释放的,当进程终止或完成对文件的访问时,内核会自动移除锁。

以下是使用C++演示文件锁的基本流程:

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    const char* filename = "example.txt";

    // 创建或打开文件
    int fd = open(filename, O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 锁定文件
    struct flock lock;
    memset(&lock, 0, sizeof(lock));
    lock.l_type = F_WRLCK; // 请求写锁
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0; // 0表示锁定整个文件

    // 使用fcntl尝试锁定文件
    if (fcntl(fd, F_SETLKW, &lock) == -1) {
        perror("fcntl F_SETLKW");
        close(fd);
        return 1;
    }

    // 临界区开始:文件已被锁定,可以安全写入
    std::cout << "File is locked for writing." << std::endl;

    // 执行写操作...
    // write(fd, "data", 4);

    // 临界区结束:解锁文件
    lock.l_type = F_UNLCK; // 请求解锁
    if (fcntl(fd, F_SETLK, &lock) == -1) {
        perror("fcntl F_SETLK");
    }

    close(fd); // 关闭文件描述符
    return 0;
}

在上述代码中,我们首先使用open函数打开或创建一个文件,并获取文件描述符fd。然后,我们设置了struct flock结构体来定义锁的参数,包括锁的类型(读锁或写锁)、起始位置、长度等。


我们使用fcntl函数与F_SETLKW命令来请求锁。F_SETLKW命令会阻塞调用进程,直到锁被成功设置。一旦获得锁,我们就可以安全地执行文件的写入操作。

完成操作后,我们将锁的类型设置为F_UNLCK,再次使用fcntl函数与F_SETLK命令来释放锁。


请注意,文件锁的行为可能会受到操作系统和文件系统的影响。在某些系统中,文件锁可能不是强制性的,这意味着其他进程可能能够忽略这些锁。在使用文件锁时,务必要确保正确地请求和释放锁,以避免死锁或资源泄露。


3、信号 (Signal)


我们还可以利用信号(Signal)在进程间传递消息,比如父进程通过信号通知子进程其已退出。不过,信号应用于IPC的情况并不常见,主要有以下两方面原因:

  • 信号遵循可靠传递原则,不会排队,可能会丢失;
  • 信号携带的信息量有限,不太适合传输大量数据。

因此,信号更多被用作进程间的"小纸条",告知对方发生了某些事件,具体数据传输还需要借助其他IPC工具。


需要详细了解信号 相关知识的通讯,请查阅往期文章:

  • Linux信号大揭秘-从中断到控制进程,一步步掌握进程通信利器!
  • linux信号集与信号掩码-保护信号处理程序,确保进程正确运行
  • linux进程家族-管理子进程,确保进程族稳健运行

四、结语


总的来说,选择IPC工具时需要考虑通信双方、数据量、同步需求等多方面因素,合理权衡就能找到最佳选择。这个领域学习曲线平缓,但内功修炼到极致,却又别有一番天地。你是否已跃跃欲试,迫不及待想投身到IPC的海洋中?本文仅作了简单探讨,若想彻底掌握该领域的精髓,不妨持续关注我们的后续分享。


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

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

相关文章

Ubuntu安装cuda

文章目录 前言一、安装NVIDIA驱动1.1 过程中的问题1.2 解决方法1.3 重启后出现 perform MOK management 二、安装Cuda2.1 检查是否安装显卡驱动2.2 安装Cuda2.3 验证CUDA是否安装成功 三、配置环境变量---未完2.4 图片居中加调整大学 总结 #pic_center 前言 只是为方便学习&…

淘宝扭蛋机源码解析:功能实现与技术细节

随着在线购物和娱乐的融合&#xff0c;淘宝扭蛋机作为一种创新的购物娱乐方式&#xff0c;受到了广大用户的喜爱。本文将深入解析淘宝扭蛋机的源码&#xff0c;探讨其功能实现与技术细节&#xff0c;以期为开发者们提供一些有价值的参考。 一、功能实现 1.用户登录与注册 淘宝…

win11通过网线分享网络到Ubuntu工控机

1.条件&#xff1a;一个能无线联网的win11&#xff0c;一根网线&#xff0c;一台Ubuntu工控机&#xff0c;并且使用网线连接两者 2.在win11电脑上 2.1 打开控制面板的网络和Internet 2.2 进入网络和共享中心&#xff0c;在左侧进入 更改适配器设置 2.3 在WLAN上右键&#xff0…

R语言数据探索和分析21-中国GDP及其影响因素多元线性回归分析

一、研究背景和意义 GDP 是宏观经济中最受关注的经济统计数字&#xff0c;目前我国国内生产总值年均增长率均明显高于同期美、日等发达经济体和巴 西、俄罗斯、南非、印度等其他金砖国家&#xff0c;成为世界经济增长的主力军&#xff0c;GDP 的增长对一个国家有着十分重要的意…

TSINGSEE青犀视频:城市道路积水智能监管,智慧城市的守护者

随着城市化进程的加快&#xff0c;城市道路网络日益复杂&#xff0c;尤其在夏季&#xff0c;由于暴雨频发&#xff0c;道路积水问题成为影响城市交通和市民生活的重要因素之一。传统的道路积水监测方式往往依赖于人工巡逻和简单的监控设备&#xff0c;这些方法存在效率低下、响…

软信天成:告别数据脏乱差!企业数据清洗实战方案分享

低质量数据普遍存在。据统计&#xff0c;数据质量问题每年给企业造成高达3.1万亿美元的损失。为了防范这种损失&#xff0c;越来越多的企业采用数据清洗来清洗数据&#xff0c;提高数据质量。 数据清洗&#xff0c;顾名思义是将数据上“脏”的部分清洗掉&#xff0c;让数据变得…

读《淘宝技术这10年》:从进化中感受技术的美与挑战

本文作者:小米,一个热爱技术分享的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货! 大家好,我是小米,一个29岁的程序员,喜欢分享技术干货。今天,我想和大家聊一聊我最近读的一本书——《淘宝技术这10年》。这本书让我深刻领悟…

JetBrains PhpStorm 激活码限时特惠 7.1 折快抢!

各位程序员&#xff0c;每天敲代码真的需要一款好用的 IDE&#xff0c;大名鼎鼎的 JetBrains 值得信赖&#xff01;PHP 开发看过来&#xff0c;PhpStorm 个人版首年订阅 618 限时特惠 7.1 折&#xff0c;有需要的朋友一定不要错过&#xff01; PhpStorm 汇集了众多效率功能和集…

【微信小程序】网络请求

出于安全性方面的考虑&#xff0c;小程序官方对数据接口的请求做出了如下两个限制&#xff1a; 只能请求HTTPS类型的接口必须将接口的域名添加到信任列表中 登录微信小程序管理后台->开发->开发设置->服务器域名->修改request合法域名。 注意事项&#xff1a; 域…

视频汇聚EasyCVR安防监控系统GA/T 1400协议视图库对接:技术实现与应用

随着信息技术的不断发展&#xff0c;各类协议标准在各个领域得到了广泛应用。GA/T1400协议作为公安视频监控系统中的一种重要标准&#xff0c;对于提升公安工作的信息化水平、加强社会治安防控具有重要意义。本文将重点探讨GA/T1400协议视图库对接的技术实现及应用价值。 一、…

领菲linfeeLNF96E多功能电力仪表智能数码液晶显示三相电压电流表

品牌 LINFEE 型号 LNF96E 货号 LNF96E 产地 中国大陆 省份 江苏省 地市 无锡市 装修及施工内容 安装工程 电源电路 交流电表 电表类型 多功能电度表 颜色分类 LNF96E-C,LNF96E-CM,LNF96E-CJ,LNF96E-CK,LNF96E-CJK,LNF96E-CMJK 多功能电力仪表,LNF96E三相多…

c语言练习:POJ 1003 宿醉(HangOver)

为什么写这篇文章 作为一名计算机相关方向的学生&#xff0c;本人的代码能力却十分差劲&#xff0c;这不能不让人万分羞愧。于是&#xff0c;决定从此好好学代码&#xff0c;每天坚持刷题。而C语言是计算机程序语言的基础&#xff0c;遂决定从c语言开始&#xff0c;提高自身编…

Nvidia/算能 +FPGA+AI大算力边缘计算盒子:中国舰船研究院

中国舰船研究院又称中国船舶重工集团公司第七研究院&#xff0c;隶属于中国船舶重工集团公司&#xff0c;是专门从事舰船研究、设计、开发的科学技术研究机构&#xff0c;是中国船舶重工集团公司的军品技术研究中心、科技开发中心&#xff1b;主要从事舰船武器装备发展战略研究…

图神经网络实战(12)——图同构网络(Graph Isomorphism Network, GIN)

图神经网络实战&#xff08;12&#xff09;——图同构网络 0. 前言1. 图同构网络原理2. 构建 GIN 模型执行图分类2.1 图分类任务2.2 PROTEINS 数据集分析2.3 构建 GIN 实现图分类2.4 GCN 与 GIN 性能差异分析 3. 提升模型性能小结系列链接 0. 前言 Weisfeiler-Leman (WL) 测试…

解决vscode终端不显示conda环境变量名称问题【详细步骤!实测可行!!】

最近在使用Visual Studio Code (VSCode) 时候&#xff0c;发现终端没有正确显示激活的conda环境名称&#xff0c;搜了一下&#xff0c;找到原因&#xff0c;记录一下&#xff0c;如果有人也遇到同样的问题&#xff0c;可以收藏一下。   分别两种情况&#xff0c;一是windows系…

GaussDB技术解读——GaussDB架构介绍(一)

目录 1 GaussDB 关键架构目标 2 GaussDB分布式架构 2.1 GaussDB 分布式关键技术架构 3 数据计算路由层&#xff08;Coordinator&#xff09;关键技术方案 3.1 分布式优化器 3.2 分布式执行框架 GaussDB是华为自主创新研发的关系型数据库&#xff0c;基于华为在数据库领域…

Python编程学习第一篇——制作一个小游戏休闲一下

到上期结束&#xff0c;我们已经学习了Python语言的基本数据结构&#xff0c;除了数值型没有介绍&#xff0c;数值型用的非常广&#xff0c;但也是最容易理解的&#xff0c;将在未来的学习中带大家直接接触和学习掌握。后续我们会开始学习这门语言的一些基础语法和编程技巧&…

C++候捷stl-视频笔记4

一个万用的hash function 哈希函数的形式&#xff0c;一种是一般函数(右边)&#xff0c;一种是成员函数(左边)&#xff0c;类的对象将成为函数对象 具体做法例子。直接把属性的所有hash值加起来&#xff0c;会在hashtable中会产生很多的碰撞&#xff0c;放在同一个bucket中的元…

嵌入式学习记录6.5(内存分配/构造函数/析构函数)

目录 目录 一.c动态内存分配回收 1.1分配 1.2回收 1.3new、delete和malloc、free之间的区别(重点&#xff09; 二.构造函数 2.1功能,格式 2.2示例 三.析构函数 3.1功能&#xff0c;格式 3.2特点 3.3示例 四.思维导图/练习 4.1思维导图 4.2练习 一.c动态内存分配回…

BGP基础配置

BGP 邻居关系建立&#xff0c;与路由条目宣告是分开的配置的 1)直连的 EBGP 邻居关系建立 [r1]bgp1 启动 BGP 协议&#xff0c;启动时需要键入 AS号&#xff1b;没有多进程概念 [r1-bgp]router-id 1.1.1.1 建议配置 RID;若不配置将自动生成一规则同 OSPF 相同 [r1-bgp]pe…