Linux | 进程间通信 | 匿名管道 | 命名管道 | 模拟代码实现进程通信

news2024/11/23 4:09:57

文章目录

    • 进程通信的意义
    • 匿名管道
      • 通信原理
      • 管道的访问控制
      • 进程控制
      • 管道的特点
    • 命名管道

进程通信的意义

之前聊进程时,讲过一个性质,即进程具有独立性,两个进程之间的交互频率是比较少的。就连父子进程也只是共享代码,修改父子进程中的任意进程的数据时,会触发写时拷贝机制,即分配一块新的空间存储数据,使父子进程做到数据分离,互不影响,父子进程之间唯一的信息交互可能就是父进程接收子进程的退出码了。所以由于进程的独立性,进程之间进行数据通信的成本较高。但是进程通信有很多的意义

1.实现数据之间的传输,进程可以发送数据给其他进程
2.实现资源的共享,多个进程共享彼此的数据
3.信息的通知,一个进程向另一个进程发送通知,告知某种特殊事件的发生
4.控制进程,一个进程可以完全控制另一个进程

进程间通信的应用很多,所以虽然它的成本高,但实现进程间通信还是很有必要的

匿名管道

通信原理

可以想象一下,两个进程要进行通信,传输数据,需要用什么方式。比如说,一个进程打开一个文件,向文件写入数据,此时的数据在内存中的缓冲区上,系统刷新缓冲区将数据写入磁盘,另一个进程也打开这个文件,系统将文件从磁盘加载到内存,使进程读取文件中的前一个进程写入的内容,这样两个进程就做到了通信。但是将外设作为通信载体会导致通信的速度非常慢,这无疑又提高了进程通信的成本,所以Linux系统中有一种特殊的文件:管道文件(操作系统将文件描述为struct_file,该结构体中有一个联合体,联合体有三个成员,分别用来表征文件类型是磁盘,管道,还是字符),管道文件不存储在磁盘上,而是存储在内存中,系统检测到文件类型为管道文件,就不用将文件数据刷新到磁盘上。用管道文件作为进程通信的载体在很大程度上提高了通信速度。

管道的最大特点是:单向传输数据,即无论何时,只能向一端输入,往另一端输出。使用管道进行进程间通信的基本步骤为

1.在父进程中以读和写的方式打开同一个文件
2.创建子进程
3.关闭父子进程中文件的读端或写端,使一个进程负责写入数据,一个进程负责读取数据

进程描述文件的结构体为struct files_struct,该结构体中有一个类型为struct file* 的fd_array数组,保存了进程打开的文件。父进程分别以读和写的方式打开一个文件(由于子进程的继承,子进程就不用再打开文件),对于父进程来说,有两个fd被存储到了fd_array数组中,其指向了进程打开的文件,创建子进程时,由于父子进程的数据以写时拷贝的方式共享,fd_array数组作为父进程的数据,子进程会继承同样的数组,而父进程打开的文件不属于进程,子进程不会继承打开的文件,所以子进程只以读和写的方式指向了父进程打开的文件,此时有四个fd(父进程的读和写,子进程的读和写)指向了打开的文件。形成管道的最后一步是关闭两个fd,使父子进程满足一个进程写入数据,一个进程读取数据的特点,形成管道。

我们要怎么打开一个管道文件?实际上系统提供了pipe接口,可以以读和写的方式打开一个管道文件在这里插入图片描述
在这里插入图片描述
pipefd作为一个输出型参数,pipe函数打开一个管道文件后,将以读方式打开的文件fd写入数组的第一个元素,将以写的方式打开的文件fd写入数组的第二个元素。所以pipefd[0]为读端,pipefd[1]为写端。该函数返回一个int值,如果调用成功返回0,失败返回-1。

管道的访问控制

使用pipe函数写一段代码

#include <iostream>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;

int main()
{
    int pipefd[2] = {0};     // 接收pipe打开的管道文件
    int ret = pipe(pipefd);  // 创建管道
    if (ret != 0)
    {
        cout << "pipe函数调用失败" << endl;
        return 1;
    }
    else
    {
        // pipe succeed  
        // 父进程写,子进程读
        int id = fork(); // 子进程的创建
        if (id == 0)
        {
            // child,reader
            close(pipefd[1]); // 写端关闭
            char readBuffer[512] = {0};
            while (1) // 死循环读取管道
            {
                memset(readBuffer, 0, sizeof(readBuffer)); // 每次读取清空缓冲区                
                ssize_t s = read(pipefd[0], readBuffer, sizeof(readBuffer) - 1);
                readBuffer[s] = '\0'; // 向缓冲区最后添加结束符
                if (s > 0)
                {
                    cout << "子进程读取到信息:" << readBuffer;
                }
                // 如果管道中没有数据,子进程不再读,退出进程
                else if (s == 0)
                {
                    cout << "父进程退出,不再写入数据" << endl;
                    close(pipefd[0]);
                    break;
                }
                else
                {
                    cout << "读取文件失败" << endl;
                    return 2;
                }
            }
            exit(1);
        }
        else if (id > 0)
        {
            // parent,writer
            // 关闭文件的读端
            close(pipefd[0]);
            char writeBuffer[128];

            int cnt = 5;
            while (cnt)
            {
                char tmp[] = "这是父进程,在与子进程进行通信";
                unsigned int t = (unsigned int)time(NULL);
                sprintf(writeBuffer, "%s time:%d\n", tmp, t);
                write(pipefd[1], writeBuffer, sizeof(writeBuffer));
                sleep(1); // 父进程每1秒写一次数据
                cnt--;
            }
            // 父进程关闭管道,此时子进程read函数将返回0
            close(pipefd[1]);
        }
        else
        {
            cout << "创建子进程失败" << endl;
            return 2;
        }
		
		// 子进程退出后,父进程等待子进程
        pid_t res = waitpid(id, NULL, 0);
        if (res > 0)
        {
            cout << "父进程等待子进程成功" << endl;
        }
    }
    return 0;
}

(pipe创建管道后,由当前进程创建子进程,子进程关闭写端,父进程关闭读端,所以父子进程中,子进程用来读取管道文件,父进程用来向管道文件写入。sleep函数使父进程写入的频率为1秒,子进程不断的读取管道中的数据,当管道中没有数据(父进程不再写入时),子进程退出,最后父进程等待子进程,回收其资源)
在这里插入图片描述
通过运行结果可以看到,父进程每隔一秒写入一次数据(时间戳相差1),子进程准确的读取了这些数据,父进程不再写入时,子进程退出,并且被父进程回收。修改上面的代码,使父进程的写入频率为3秒,观察子进程运行的结果
在这里插入图片描述
可以看到子进程还是成功的读取了这些数据,无论父进程写入管道的时间间隔为多少。那么子进程读取完管道的数据,在父进程下一次写入数据前,子进程是什么状态?

如果父子进程打开一个普通文件,并向其写入数据,父子进程的写入是没有先后顺序的,无法确定谁先向文件中写入,这样的写入是无序且混乱的。但是在管道的数据通信中,父子进程的读写顺序是有序的,通过上面出现的运行结果,可以推测:如果管道中没有数据,reader就会阻塞(进入阻塞状态,进入文件的等待队列),等待数据的写入,如果管道被写满了,writer也会阻塞,等待数据被读取。
在这里插入图片描述
修改代码,父进程不断的写管道写入数据,子进程调用sleep休眠100秒,不再读取数据,父进程写入数据时会向屏幕打印写入的次数,运行程序,父进程向管道写入513次后就阻塞了,因为子进程没有读取管道的数据,管道被父进程写满了,此时的父进程不再写入,而是等待管道的数据被读取。

所以,在管道文件中,进程的读写顺序是遵循一种访问机制的,我们把这种访问机制叫做同步和互斥机制。这种机制可以保证管道的每次读和写都是有效的,不会出现重复的读取或者多次写入覆盖之前数据的情况。

进程控制

利用管道可以实现一个进程控制多个进程的效果。

刚才的一份代码可以实现父进程向管道写入数据,子进程读取数据,如果父进程写入的不再是数据,而是需要子进程执行的方法呢?子进程就可以获取这个方法并执行该方法,也就是说在进程间通信的数据,可以是普通的文本,也可以是一个方法,还可以是其他数据,我们的目的是让子进程得到父进程传递的数据,这也是进程通信的本质:使不同进程看到同一份数据。如果传递的数据是一个方法,我们只需要修改代码,让子进程执行此方法就能做到进程控制

而管道作为进程间通信的载体,父进程向子进程传递数据就成了向管道传递数据,只要子进程能从管道中读取数据,进程间通信就能实现。所以进程间通信的重点在于进程与管道间的数据传输。

如果需要通信的对象是多个子进程,也就需要父进程向多个管道中写数据。其大致过程为:首先要有需要让子进程执行的任务,也就是函数,函数以函数指针的方式存储在一个数组中。假设父进程需要与5个子进程进行通信,这就意味着需要使用pipe函数创建5个管道,采用for循环,循环5次,每次循环pipe函数都会打开一个管道,管道打开后创建子进程,关闭子进程的写端与父进程的读端,接着让子进程进入阻塞,等待管道被父进程写入数据,当数据被写入时,分析这些数据得到需要执行的任务并执行。父进程需要向子进程分发任务,由于现在在使用for循环创建管道,通信的对象(子进程)还没有完全创建好,所以此时不能直接向子进程分发任务,需要记录子进程的进程pid(用于父进程等待子进程,回收其资源)与管道文件的fd写端(用于父进程向管道的写入),在管道创建完成后(for循环之后),根据保存的fd随机向5个管道中的一个管道写入数据,也就是派发任务,当父进程不再需要向子进程派发任务,关闭管道文件的写端,使子进程不再阻塞等待数据被写入管道,而直接退出进程,最后由父进程回收子进程资源。

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <ctime>
#include <cstring>
#include <unistd.h>
#include <stdio.h>
#include <vector>
#include <map>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <string>

using namespace std;

typedef void (*function)();
vector<function> functions;        // 全局的方法集,存储需要执行的任务
map<uint32_t, string> information; // 存储关于方法的信息

void function1()
{
    cout << "本次执行function1任务,执行本次任务的进程id[ " << getpid() << " ]" << endl;
}

void function2()
{
    cout << "本次执行function2任务,执行本次任务的进程id[ " << getpid() << " ]" << endl;
}

void function3()
{
    cout << "本次执行function3任务,执行本次任务的进程id[ " << getpid() << " ]" << endl;
}

void loadFunctions() // 方法集的加载
{
    information.insert({functions.size(), "function1"});
    functions.push_back(function1);

    information.insert({functions.size(), "function2"});
    functions.push_back(function2);

    information.insert({functions.size(), "function2"});
    functions.push_back(function3);
}

void work(uint32_t readfd)
{
    cout << "进程[" << getpid() << "]" << " 开始工作" << endl;
    while (1) // 不断的读取管道数据
    {
        int task_id;
        memset(&task_id, -1, sizeof(task_id));
        int ret = read(readfd, &task_id, sizeof(task_id)); // 读取任务在方法集中的task_id
        if (ret == 0)                                      // 管道中没有数据
        {
            cout << "由于父进程不再写入,子进程[ " << getpid() << "]不再读取" << endl;
            sleep(1);
            break;
        }
        if (ret != sizeof(task_id))
        {
            cout << "读取方法出错" << endl;
            return;
        }

        if (task_id >= 0 && task_id < functions.size()) // 获取的方法在方法集中
        {
            sleep(0.1);
            functions[task_id](); // 执行该方法
        }
        else
        {
            cout << "no function" << endl;
            return;
        }
    }
    cout << "进程[" << getpid() << "]" << " 结束工作" << endl;
}

// [pid, fd]
typedef pair<uint32_t, uint32_t> elem;

void sendTask(const vector<elem> &task_vector)
{
	// 随机派发任务,做到负载均衡
    srand((unsigned int)time(nullptr));
    // 派发任务个数为5个
    int cnt = 5;
    while (cnt)
    {
        sleep(1);

        // 随机挑选一个子进程
        int process = rand() % task_vector.size();

        // 随机挑选一个任务
        int task_id = rand() % functions.size();

        // 任务的派发
        write(task_vector[process].second, &task_id, sizeof(task_id));

        cout << "父进程派发任务" << information[task_id] << "给pid[ " << task_vector[process].first << " ]的子进程" << endl;

        cnt--;
    }
}

int main()
{
    // 任务表,记录子进程的pid和对应的fd写端文件
    vector<elem> task_vector;
    loadFunctions();
    int processNum = 5; // 子进程个数
    for (int i = 0; i < processNum; i++)
    {
        // 管道的创建
        int pipefd[2] = {0};
        ssize_t ret = pipe(pipefd);
        if (ret != 0) // 管道创建失败
        {
            cout << "pipe fail" << endl;
            return 1;
        }

        // 子进程的创建
        int id = fork();
        if (id == 0)
        {
            // child,reader
            close(pipefd[1]);
            // 子进程接收任务,执行
            work(pipefd[0]);
            close(pipefd[0]);
            exit(0); // 子进程退出
        }

        // parent,writer
        close(pipefd[0]);
        elem e = {id, pipefd[1]};
        task_vector.push_back(e); // 每循环一次,就记录子进程的pid和fd写端在任务表上
    }
    sleep(1);
    cout << "create all process success!" << endl;
    sendTask(task_vector);    // 派发任务
	
	// 需要提前将所有管道的写端关闭,不能边回收子进程边关闭写端
    for (int i = 0; i < task_vector.size(); i++)
    {
        close(task_vector[i].second);
    }

    // 回收子进程
    for (int i = 0; i < task_vector.size(); i++)
    {
        close(task_vector[i].second);
        // 关闭管道的写端,使子进程不再阻塞等待管道的写入,直接退出
        if (waitpid(task_vector[i].first, nullptr, 0) > 0)
        {
            cout << "等待子进程[ " << task_vector[i].first << " ]成功" << endl;
        }
        sleep(1);
    }
    return 0;
}

代码较长,建议从main函数开始看起,理解父进程派发任务的大致过程然后再理解其中的函数(loadFunctions,work,sendTask),整份代码中的cout只是为了更好的观察代码运行逻辑。
在这里插入图片描述
运行程序,父进程成功地派发了5个任务给子进程,并且任务与进程都是随机选择的,不会使一个进程执行多次任务,最后关闭管道的写端后,子进程成功地退出并且被父进程回收了。对于这份代码,我认为的重点是:关闭管道的写端,子进程调用read函数读取管道数据,read会返回0,这里需要进行判断,read返回0说明父进程不再写入,子进程就可以退出了。以及创建管道与子进程时,用task_vector任务表记录子进程的pid和fd写端,任务表在这里的使用是一个重点,使用任务表才能向子进程派发任务以及回收子进程的资源。

管道的特点

匿名管道只能用于具有血缘关系的进程间,常用于父子进程间通信
管道的数据传输方向是单向的,这是由内核决定的
管道自带访问控制机制(同步互斥机制)
管道的生命周期是跟随进程的,当最后一个打开管道的进程退出,系统释放进程的资源,管道的资源也随之被释放
管道是面向字节流的,数据的传输以字节为单位

刚才聊的管道其实叫做匿名管道,在shell命令行中也有匿名管道的存在,使用竖线“|”就能调用命令行中的匿名管道。在一个会话中输入sleep 20000 | sleep 10000就调用了匿名管道,在另一个会话中输入ps axj指令过滤出含有sleep的进程信息,观察信息可以发现sleep 20000 | sleep 10000这行命令调用了两个进程,并且它们的父进程是相同的,所以这是两个兄弟进程。与我们测试匿名管道的代码不同,命令行中的管道不是父子进程间通信,而是两个兄弟进程间通信,大概实现就是父进程fork两次创建了两个子进程,接着父进程关闭管道的读端与写端,两个子进程各自关闭读端和写端,使一个进程向管道写入数据,一个进程读取管道的数据。并且命令行中的管道还有一个特点,就是前一个进程的输出是后一个进程的输入,所以系统会将前一个进程的输出重定向到后一个进程的输入。

在这里插入图片描述
匿名管道作为管道的一种,其特征是只能用于具有血缘关系的进程间通信,要使任意进程进行通信,就不能使用匿名管道,而需要使用命名管道。

命名管道

在这里插入图片描述
使用mkfifo函数可以创建一个命名管道。命名管道的本质是磁盘上的一个FIFO文件,文件被存储在磁盘上,就意味着文件有一个绝对路径,其他进程可以通过这个路径找到这个文件,这个路径就像一个标识符,使不同进程可以通过这个标识符找到这个管道文件,以这个管道文件进行通信。虽然命名管道是磁盘上的文件,但该文件的数据不用向磁盘刷新,即磁盘上的数据只是一个符号,用来表示该文件的存在,与匿名管道一样,命名管道实际的读写是在内存中,以内存为载体进行通信的速度明显比以外设为载体进行通信快。

函数有两个参数,pathname是fifo文件所在路径,mode是该文件的权限,关于权限通常用一串八进制数表示。

// serve.cc文件
#include "ipc.h"
using namespace std;

int main()
{
    umask(0);
    if (mkfifo(IPC_PATH, 0600) != 0) // 创建命名管道失败
    {
        cerr << "fifofile fail" << endl;
        return 1;
    }
    
    int fd = open(IPC_PATH, O_RDONLY);
    if (fd == -1)
    {
        cerr << "open fail" << endl;
        return 1;
    }

    char read_buffer[512];
    while (1)
    {
        memset(read_buffer, 0, sizeof(read_buffer));
        ssize_t s = read(fd, read_buffer, sizeof(read_buffer) - 1);
        if (s == 0)
        {
            cout << "服务退出" << endl;
            // 关闭文件的读端
            close(fd);
            // 删除磁盘上的管道文件
            unlink(IPC_PATH);
            break;
        }
        read_buffer[strlen(read_buffer) - 1] = '\0';
        cout << read_buffer << endl;
    }
    return 0;
}

// client.cc文件
#include "ipc.h"
using namespace std;
int main()
{
   
    int fd = open(IPC_PATH, O_WRONLY);
    if (fd == -1)  // 打开文件失败
    {
        cerr << "open fail" << endl;
        return 2;
    }
    char write_buffer[128];
    while (1)
    {
        cout << "请输入信息#";
        fflush(stdout);
        // 每次写入数据清空write_buffer
        memset(write_buffer, 0, sizeof(write_buffer));
        // 从键盘获取数据失败
        if (fgets(write_buffer, sizeof(write_buffer), stdin) == nullptr)
        {
            cerr << "input fail" << endl;
            return 3;
        }
        // 当输入exit表示客户的退出,服务器也停止读取数据
        if (strcmp(write_buffer, "exit\n") == 0)
        {
            break;
        }
        // 向管道写入从键盘上获取的数据
        write(fd, write_buffer, sizeof(write_buffer));
    }
    cout << "客户退出" << endl;
    // 关闭文件的读端,释放管道资源
    close(fd);
    return 0;
}

模拟命名管道的使用:现在有一个客户端,一个服务器,客户向服务器发送请求,服务器需要接收请求。由服务器创建命名管道进行通信,服务器是命名管道的读端,以读的方式打开管道,客户则以写的方式打开管道,当然管道的路径是事先约定好的,双方都知道管道在哪。客户发送消息,当发送消息为"exit"时表示不再发送数据,关闭管道文件(但文件以引用计数的方式进行资源的释放,由于客户和服务器都打开了该文件,所以文件的引用计数为2,客户关闭了该文件只是引用计数-1,并没有释放管道资源,当服务器关闭文件时,引用计数减到0,管道才会进行资源的释放),此时的服务器read函数返回0,服务器根据这一条件关闭管道,最终释放管道资源。但是命名管道本质是内存中的一个文件,调用unlink删除内存中的fifo文件才会将管道资源彻底的释放。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小程序云开发笔记一

一、什么是云开发&#xff1f; 微信官方云原生开发平台&#xff0c;腾讯云的各种能力加持&#xff0c;用云开发开发者可以节省大量的开发时间和运维成本。 二、云开发优势 无需运维&#xff0c;数据变大不需要管理&#xff0c; 弹性伸缩&#xff0c;业务量变大&#xff0c;服…

在一台电脑上安装多个python版本(小白教程)

我自己的办公电脑是64位&#xff0c;好几个同事的电脑还是win7&#xff0c;32位&#xff0c;因此我在写python办公自动化的时候还要考虑32位的python&#xff0c;因此在电脑上安装了两个版本的python&#xff0c;方便测试、打包使用 1、首先&#xff0c;下载两个python&#xf…

代码随想录算法训练营第七天|LeetCode 454. 四数相加 II 、383. 赎金信、 15. 三数之和、18. 四数之和

LeetCode 454. 四数相加 II 题目链接&#xff1a;454. 四数相加 II 分析&#xff1a; 本题比较简单&#xff0c;因为是无关的四个数组&#xff0c;所以不需要考虑去重&#xff0c;所以用哈希比较简单 思路&#xff1a; 定义个无序map先将nums1和nums2的和的数都存进去&…

单目标应用:世界杯优化算法(World Cup Optimization,WCO)求解单仓库多旅行商问题SD-MTSP(可更改旅行商个数及起点)

一、世界杯优化算法 世界杯优化算法&#xff08;World Cup Optimization&#xff0c;WCO)由Navid Razmjooy等人于2016年提出&#xff0c;该算法模拟了国际足联世界杯比赛&#xff0c;思路新颖&#xff0c;收敛速度快&#xff0c;全局寻优能力强。 算法原理参考&#xff1a;智…

[附源码]java毕业设计学生宿舍管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

新型智能优化算法——海鸥优化算法(基于Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

基于莱维飞行扰动策略的麻雀搜索算法(ISSA)(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

【强化学习论文合集】ICML-2022 强化学习论文 | 2022年合集(二)

强化学习(Reinforcement Learning, RL),又称再励学习、评价学习或增强学习,是机器学习的范式和方法论之一,用于描述和解决智能体(agent)在与环境的交互过程中通过学习策略以达成回报最大化或实现特定目标的问题。 本专栏整理了近几年国际顶级会议中,涉及强化学习(Rein…

[附源码]java毕业设计校园摄影爱好者交流网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

elasticsearch bucket 之rare terms聚合

文章目录1、背景2、需求3、前置准备3.1 准备mapping3.2 准备数据4、实现需求4.1 dsl4.2 java代码4.3 运行结果5、max_doc_count 和 search.max_buckets6、注意事项7、完整代码8、参考文档1、背景 我们知道当我们使用 terms聚合时&#xff0c;当修改默认顺序为_count asc时&…

nodejs+mysql+vscode网上图书商城销售管理系统vue

当前社会是一个网络高度发达的社会&#xff0c;人们都处在互联网时代中&#xff0c;对于知识的获取都是通过互联网&#xff0c;为了鼓励人们积极获取纸质知识&#xff0c;我想要设计一个网上图书售卖系统。这个系统设计的目的是为了方便人们们作为参考资料. 网上图书管理系统的…

【31-业务开发-基础业务-品牌管理-级联类别信息业务功能实现-品牌管理和商品分类管理俩者业务关联出现数据冗余,导致数据不同步的问题-开启事务-项目测试】

一.知识回顾 【0.三高商城系统的专题专栏都帮你整理好了&#xff0c;请点击这里&#xff01;】 【1-系统架构演进过程】 【2-微服务系统架构需求】 【3-高性能、高并发、高可用的三高商城系统项目介绍】 【4-Linux云服务器上安装Docker】 【5-Docker安装部署MySQL和Redis服务】…

进 4 球得 1 分,阿根廷败北背后的科技与狠活

内容一览&#xff1a;11 月 22 日&#xff0c;世界杯 C 组首场比赛&#xff0c;沙特阿拉伯 2:1 反超阿根廷&#xff0c;今天我们将逐一盘点阿根廷进 4 球得 1 分背后的科技与狠活。 关键词&#xff1a;世界杯 VAR 半自动越位技术 沙特爆冷逆袭&#xff0c;2:1 反超阿根廷 2022…

如何改进企业旧式工时管理系统?

工时管理系统对企业很重要&#xff0c;特别是那些不太明显的知识性工作的企业。 一些企业仍在使用基于纸张的工时表管理&#xff0c;这通常会带来以下问题&#xff1a; ● 过程非常耗时 ● 人为错误的风险很高 ● 有道德上的漏洞&#xff0c;如同伴帮打卡和时间盗窃 ● 数据处…

【强化学习论文合集】AAAI-2022 强化学习论文合集(附论文链接)

强化学习&#xff08;Reinforcement Learning, RL&#xff09;&#xff0c;又称再励学习、评价学习或增强学习&#xff0c;是机器学习的范式和方法论之一&#xff0c;用于描述和解决智能体&#xff08;agent&#xff09;在与环境的交互过程中通过学习策略以达成回报最大化或实现…

【测试沉思录】16. 性能测试中的系统资源分析之三:磁盘

作者&#xff1a;马海琴 编辑&#xff1a;毕小烦 三. 磁盘 磁盘是可以持久化存储的设备&#xff0c;根据存储介质的不同&#xff0c;常见磁盘可以分为两类&#xff1a;机械磁盘和固态磁盘。磁盘就像人的大脑皮层&#xff0c;负责数据的储存、记忆。 磁盘对于服务器来说十分重…

大学生静态HTML网页源码——佛山旅游景点介绍网页代码 家乡旅游网页制作模板 web前端期末大作业

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

学生个人网页模板 简单个人主页--贝聿铭人物介绍 6页带表单 带报告3800字

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | ‍个人博客网站 | ‍个人主页介绍 | 个人简介 | 个人博客设计制作 | 等网站的设计与制作 | 大学生个人HTML网页设计作品 | HTML期末大学生网页设计作业…

c语言:初识指针(二)

初识指针一.野指针1.野指针形成原因一是&#xff1a;未初始化2.野指针形成原因二&#xff1a;指针越界3.野指针形成原因三&#xff1a;指针所指向的内存空间被释放二.指针的运算1.指针-整数运算2.指针-指针3.指针的关系运算三.指针和数组四.二级指针五.指针数组1.定义2.用一维数…

APS生产计划排产在装备制造业的应用

装备制造业是对所有为国民经济各部门的简单再生产和扩大再生产提供技术装备的制造工业的总称&#xff0c;范围包括航空、航天、军工制造&#xff1b;民用飞机、铁路、船舶、汽车等先进交通运输设备制造&#xff1b;石油、矿产、化工、压力容器、电力成套设备制造&#xff1b;以…