《Linux从练气到飞升》No.22 Linux 进程间通信

news2024/11/6 9:58:13

🕺作者: 主页

我的专栏
C语言从0到1
探秘C++
数据结构从0到1
探秘Linux
菜鸟刷题集

😘欢迎关注:👍点赞🙌收藏✍️留言

🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!

文章目录

    • 前言
    • 理解进程通信
      • 进程间通信的必要性
      • 进程间通信的技术背景
      • 进程间通信的本质理解
      • 进程间通信标准和分类
    • 管道
      • 什么是管道
      • 管道的原理
      • 用fork来共享管道原理
      • 站在文件描述符角度-深度理解管道
      • 匿名管道
      • image.png
      • 一个小demo来理解管道
      • 管道特点
      • 管道读写规则
      • 匿名管道实现一个小的进程池demo
        • ProcessPool.cc
        • Task.hpp
        • makefile
      • 命名管道
      • 创建一个命名管道
        • 命名管道可以从命令行上创建
        • 命名管道也可以从程序里创建
      • 匿名管道与命名管道的区别
      • 命名管道的打开规则
      • 命名管道实现server&client通信
        • server.cc
        • client.cc
        • comm.hpp
        • Log.hpp
        • makefile
    • system V 共享内存
      • 共享内存示意图
      • 共享内存的原理
      • 共享内存数据结构
      • 共享内存函数
        • shmget函数
        • shmid vs key
        • shmat函数
        • 说明:
        • shmdt函数
        • shmctl函数
      • 共享内存的要点
        • 为什么通过共享内存传输的速度最快?
        • 基于对共享内存的理解
        • 一些概念

前言

Linux 进程间通信是操作系统中非常重要的一个概念,它允许不同进程之间进行数据交换和协作,从而实现更高效、更复杂的应用程序。在计算机科学领域,进程间通信是一个广泛研究的主题,也是操作系统设计中的核心问题之一。

本文将介绍 Linux 中常见的几种进程间通信方式,包括管道、命名管道、共享内存等。我们将深入探讨每种方式的工作原理、优缺点以及适用场景,帮助读者更好地理解和运用这些技术。

无论你是初学者还是资深开发人员,本文都将为你提供有价值的知识和实用的技能。让我们开始吧!

理解进程通信

进程间通信的必要性

单进程无法使用并发能力,更加无法实现多进程协同
有的是为了传输数据、同步执行流、消息通知等,这些都需要多进程协同才能实现
它不是目的,而是手段

进程间通信的技术背景

1.进程是具有独立性的。虚拟地址空间+页表 保证进程运行的独立性
2.因为他的独立性导致通信成本会比较高

进程间通信的本质理解

1.进程间通信的前提是让不同的进程看到同一块“内存”(特定的结构组织)
2.同一块“内存”不能隶属于任何一个进程,而应该强调共享

进程间通信标准和分类

进程间通信方式的一些标准和分类

  • 管道
    • 匿名管道pipe
    • 命名管道
  • System V IPC 主要做的单机通信
    • System V 消息队列
    • System V 共享内存
    • System V 信号量
  • POSIX IPC 主要做的网络通信
    • 消息队列
    • 共享内存
    • 信号量
    • 互斥量
    • 条件变量
    • 读写锁

标准在我们使用者看来都是接口上的理解

管道

什么是管道

比如天然气、石油等管道,它只能单向输出内容,它能传输资源
现在在计算机中最重要的资源是数据,数据是互联网时代的“石油”

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
  • 管道由系统提供

image.png

管道的原理

管道通信背后是进程之间通过管道进行通信
举例:
1.父进程以读写的方式打开一个文件
2.fork()创建子进程
3.双方各自关闭自己不需要的文件描述符

  • 此时子进程会自己拷贝父进程的文件描述符表
  • 此时子进程的也会指向父进程以读写的方式打开的那个文件
  • 此时就有两个进程共享一个文件了
  • 假设使父进程写入,子进程读取
  • 这个数据流就是一个“管道”

它不会把数据存到磁盘上,都在内存中
image.png

用fork来共享管道原理

image.png

站在文件描述符角度-深度理解管道

image.png

匿名管道

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

image.png

一个小demo来理解管道

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

// 为什么不定义全局buffer来进行通信呢?? 因为有写时拷贝的存在,无法更改通信!

int main()
{
    // 1. 创建管道
    int pipefd[2] = {0}; // pipefd[0(嘴巴,读书)]: 读端 , pipefd[1(钢笔,写)]: 写端
    int n = pipe(pipefd);
    assert(n != -1); // debug assert, release assert
    (void)n;//避免n只被定义没有被使用导致大量报红

#ifdef DEBUG
    cout << "pipefd[0]: " << pipefd[0] << endl; // 3
    cout << "pipefd[1]: " << pipefd[1] << endl; // 4
#endif
    // 2. 创建子进程
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        //子进程 - 读
        // 3. 构建单向通信的信道,父进程写入,子进程读取
        // 3.1 关闭子进程不需要的fd
        close(pipefd[1]);
        char buffer[1024 * 8];
        while (true)
        {
            // sleep(20);
            // 写入的一方,fd没有关闭,如果有数据,就读,没有数据就等
            // 写入的一方,fd关闭, 读取的一方,read会返回0,表示读到了文件的结尾!
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
            //读取pipefd[0]文件描述符,到buffer中,读取sizeof(buffer) - 1个
            //最后一个位置自己设置\0
            if (s > 0)
            {
                buffer[s] = 0;
                cout << "child get a message[" << getpid() << "] Father# " << buffer << endl;
            }
            else if(s == 0)
            {
                cout << "writer quit(father), me quit!!!" << endl;
                break;
            }
        }
        // close(pipefd[0]);
        exit(0);
    }
    //父进程 - 写
    // 3. 构建单向通信的信道
    // 3.1 关闭父进程不需要的fd
    close(pipefd[0]);
    string message = "我是父进程,我正在给你发消息";
    int count = 0;
    char send_buffer[1024 * 8];
    while (true)
    {
        // 3.2 构建一个变化的字符串
        snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",
                 message.c_str(), getpid(), count++);
        // 3.3 写入
        write(pipefd[1], send_buffer, strlen(send_buffer));
        // 3.4 故意sleep
        sleep(1);
        cout << count << endl;
        if (count == 5){
            cout << "writer quit(father)" << endl;
            break;
        }
    }
    close(pipefd[1]);
    pid_t ret = waitpid(id, nullptr, 0);
    cout << "id : " << id << " ret: " << ret <<endl;
    assert(ret > 0); 
    (void)ret;

    return 0;
}

管道特点

  1. 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
    1. 此时子进程会继承父进程的文件描述符表
  2. 管道提供流式服务
    1. 它是buffer里面有多少就读多少,可能一次读取你多次写入的内容
  3. 一般而言,进程退出,管道释放,所以管道的生命周期随进程
    1. 管道是基于文件的,文件的生命周期是随进程的,那么管道的生命周期也是随进程的
  4. 一般而言,内核会对管道操作进行同步与互斥
    1. 管道能让进程间协同,它具有访问控制,因为它会有等待,当写入写满时还没读取,就会等待读取,否则再写入会覆盖buffer中的内容
    2. 同步和互斥以后再讲
  5. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
    1. 半双工不是说只能读或只能写,它的含义是暂时只用这个功能

管道读写规则

  • 当没有数据可读时
    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  • 当管道满的时候
    • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0,标识读到了文件结尾
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

匿名管道实现一个小的进程池demo

ProcessPool.cc
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"

#define PROCESS_NUM 5
//进程个数

using namespace std;

int waitCommand(int waitFd, bool &quit) //如果对方不发,我们就阻塞
{
    uint32_t command = 0;
    ssize_t s = read(waitFd, &command, sizeof(command));
    if (s == 0)
    {
        quit = true;
        return -1;
    }
    assert(s == sizeof(uint32_t));
    return command;
}

void sendAndWakeup(pid_t who, int fd, uint32_t command)
{
    write(fd, &command, sizeof(command));
    cout << "main process: call process " << who << " execute " << desc[command] << " through " << fd << endl;
}

int main()
{
    // 代码中关于fd的处理,有一个小问题,不影响我们使用,但是你能找到吗??
    load();
    // pid: pipefd
    vector<pair<pid_t, int>> slots;
    // 先创建多个进程
    for (int i = 0; i < PROCESS_NUM; i++)
    {
        // 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        pid_t id = fork();
        assert(id != -1);
        // 子进程我们让他进行读取
        if (id == 0)
        {
            // 关闭写端
            close(pipefd[1]);
            // child
            while (true)
            {
                // pipefd[0]
                // 等命令
                bool quit = false;
                int command = waitCommand(pipefd[0], quit); //如果对方不发,我们就阻塞
                if (quit)
                    break;
                // 执行对应的命令
                if (command >= 0 && command < handlerSize())
                {
                    callbacks[command]();
                }
                else
                {
                    cout << "非法command: " << command << endl;
                }
            }
            exit(1);
        }
        // father,进行写入,关闭读端
        close(pipefd[0]); // pipefd[1]
        slots.push_back(pair<pid_t, int>(id, pipefd[1]));
    }
    // 父进程派发任务
    srand((unsigned long)time(nullptr) ^ getpid() ^ 23323123123L); // 让数据源更随机
    while (true)
    {
        // 选择一个任务, 如果任务是从网络里面来的?
        int command = rand() %  handlerSize();
        // 选择一个进程 ,采用随机数的方式,选择进程来完成任务,随机数方式的负载均衡
        int choice = rand() % slots.size();
        // 把任务给指定的进程
        sendAndWakeup(slots[choice].first, slots[choice].second, command);
        sleep(1);

        // int select;
        // int command;
        // cout << "############################################" << endl;
        // cout << "#   1. show funcitons      2.send command  #" << endl;
        // cout << "############################################" << endl;
        // cout << "Please Select> ";
        // cin >> select;
        // if (select == 1)
        //     showHandler();
        // else if (select == 2)
        // {
        //     cout << "Enter Your Command> ";
        //     // 选择任务
        //     cin >> command;
        //     // 选择进程
        //     int choice = rand() % slots.size();
        //     // 把任务给指定的进程
        //     sendAndWakeup(slots[choice].first, slots[choice].second, command);
        // }
        // else
        // {
        // }
    }

    // 关闭fd, 所有的子进程都会退出
    for (const auto &slot : slots)
    {
        close(slot.second);
    }

    // 回收所有的子进程信息
    for (const auto &slot : slots)
    {
        waitpid(slot.first, nullptr, 0);
    }
}
Task.hpp
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <unistd.h>
#include <functional>

typedef std::function<void()> func;

std::vector<func> callbacks;
std::unordered_map<int, std::string> desc;

void readMySQL()
{
    std::cout << "sub process[" << getpid() << " ] 执行访问数据库的任务\n" << std::endl;
}

void execuleUrl()
{
    std::cout << "sub process[" << getpid() << " ] 执行url解析\n" << std::endl;
}

void cal()
{
    std::cout << "sub process[" << getpid() << " ] 执行加密任务\n" << std::endl;
}

void save()
{
    std::cout << "sub process[" << getpid() << " ] 执行数据持久化任务\n" << std::endl;
}

void load()
{
    desc.insert({callbacks.size(), "readMySQL: 读取数据库"});
    callbacks.push_back(readMySQL);

    desc.insert({callbacks.size(), "execuleUrl: 进行url解析"});
    callbacks.push_back(execuleUrl);

    desc.insert({callbacks.size(), "cal: 进行加密计算"});
    callbacks.push_back(cal);

    desc.insert({callbacks.size(), "save: 进行数据的文件保存"});
    callbacks.push_back(save);
}

void showHandler()
{
    for(const auto &iter : desc )
    {
        std::cout << iter.first << "\t" << iter.second << std::endl;
    }
}

int handlerSize()
{
    return callbacks.size();
}
makefile
ProcessPool:ProcessPool.cc
	g++ -o $@ $^ -std=c++11 #-DDEBUG
.PHONY:clean
clean:
	rm -f ProcessPool

命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件
  • 文件在系统中的路径具有唯一性,双方进程可以通过命名管道文件的路径看到同一份资源,这样就可以和匿名管道实现同样的效果

创建一个命名管道

命名管道可以从命令行上创建

命令行方法是使用下面这个命令:

mkfifo filename

image.png

命名管道也可以从程序里创建

相关函数有:

int mkfifo(const char *filename,mode_t mode);

创建命名管道:

int main(int argc, char *argv[])
{
    mkfifo("p2", 0644);
    return 0;
}

匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

命名管道的打开规则

如果当前打开操作是为读而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  • O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

命名管道实现server&client通信

server.cc
#include "comm.hpp"
#include <sys/wait.h>

static void getMessage(int fd)
{
    char buffer[SIZE];
    while (true)
    {
        memset(buffer, '\0', sizeof(buffer));
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            cout <<"["  << getpid() << "] "<< "client say> " << buffer << endl;
        }
        else if (s == 0)
        {
            // end of file
            cerr <<"["  << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;
            break;
        }
        else
        {
            // read error
            perror("read");
            break;
        }
    }
}

int main()
{
    // 1. 创建管道文件
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
        perror("mkfifo");
        exit(1);
    }

    Log("创建管道文件成功", Debug) << " step 1" << endl;

    // 2. 正常的文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(2);
    }
    Log("打开管道文件成功", Debug) << " step 2" << endl;

    int nums = 3;
    for (int i = 0; i < nums; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            // 3. 编写正常的通信代码了
            getMessage(fd);
            exit(1);
        }
    }
    for(int i = 0; i < nums; i++)
    {
        waitpid(-1, nullptr, 0);
    }
    // 4. 关闭文件
    close(fd);
    Log("关闭管道文件成功", Debug) << " step 3" << endl;

    unlink(ipcPath.c_str()); // 通信完毕,就删除文件
    Log("删除管道文件成功", Debug) << " step 4" << endl;

    return 0;
}
client.cc
#include "comm.hpp"

int main()
{
    // 1. 获取管道文件
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(1);
    }

    // 2. ipc过程
    string buffer;
    while(true)
    {
        cout << "Please Enter Message Line :> ";
        std::getline(std::cin, buffer);
        write(fd, buffer.c_str(), buffer.size());
    }

    // 3. 关闭
    close(fd);
    return 0;
}
comm.hpp
#ifndef _COMM_H_
#define _COMM_H_

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"

using namespace std;

#define MODE 0666
#define SIZE 128

string ipcPath = "./fifo.ipc";


#endif
Log.hpp
#ifndef _LOG_H_
#define _LOG_H_

#include <iostream>
#include <ctime>

#define Debug   0
#define Notice  1
#define Warning 2
#define Error   3


const std::string msg[] = {
    "Debug",
    "Notice",
    "Warning",
    "Error"
};

std::ostream &Log(std::string message, int level)
{
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    return std::cout;
}


#endif
makefile
.PHONY:all
all:client mutiServer

client:client.cxx
	g++ -o $@ $^ -std=c++11
mutiServer:server.cxx
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f client mutiServer

system V 共享内存

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
查看共享内存资源的脚本命令:while : ; do ipcs -m; sleep 1; done
image.png

  • key:这个共享内存段名字,key的作用是作为标识,让通信的对方进程找到我创建的共享内存
  • shmid: 共享内存标识
  • perms:共享内存的权限
  • bytes:共享内存的大小
  • nottch:共享内存上连接的进程数

共享内存示意图

image.png

共享内存的原理

它的本质上是一块内存被映射给了两个进程,这两个进程从而能够实现进程通信,前面讲的管道也是类似的原理,所以也能用来通信,但它们的本质上来说都是共享同一个文件。
共享内存是操作系统提供的,操作系统当然要管理共享内存,所以当我们申请共享内存的时候,得到不仅仅是共享内存块,还有管理共享内存的内核数据结构。

共享内存数据结构

struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};

共享内存函数

shmget函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字,key的作用是作为标识,让通信的对方进程找到我创建的共享内存,
它是多少不重要,但是必须要在系统中唯一即可。
怎么取得唯一的key?
需要使用ftok函数来生成一个唯一的key
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码shmid,类似曾经的fd;失败返回-1

shmflg常用两个选项:
1.IPC_CREAT:单独使用时,如果创建共享内存,如果底层已经存在,则获取后返回,
如果不存在,就创建后返回。
2.IPC_EXCL:单独使用没有意义
3.IPC_CREAT和IPC_EXCL一起使用:底层不存在就创建,底层存在出错返回,
也就是说返回成功一定是一个全新的shm
shmid vs key

1.只有创建的时候用key,大部分情况用户访问共享内存都用的是shmid
2.key可以标识共享内存在系统层面的唯一性,shmid可以标识共享内存在用户层面上的唯一性
2.当进程运行结束后,共享内存依旧存在,system V IPC资源的生命周期随内核
3.想要删除可以手动删除,命令是ipcrm -m <shmid>,也可以代码删除,使用函数shmctl

shmat函数
功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1
说明:
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。
公式:shmaddr -(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

image.png
当为IPC_RMID选项时,即使有进程连接了共享内存,它也会删除

共享内存的要点

为什么通过共享内存传输的速度最快?

image.png

如上图所示,当我们使用其他的方式传输数据、信号的时候,会通过内核的处理再转给另一端,这之间会经历多次拷贝。
比如使用键盘写入,然后打印在显示器上,会经历4次拷贝如果直接使用共享内存的方式来进行相同的操作就省去了内核的处理,少了两次拷贝。

基于对共享内存的理解

为了让进程间通信,需要让不同的进程之间看到同一份资源,在此之前讲的所有的通信方式本质都是优先解决一个问题:让不同的进程之间看到同一份资源
但是使用共享内存来让不同的进程之间看到同一份资源时会带来一些时序问题,造成数据不一致的问题,比如写端只写了一半就被读取可能会造成语义相反等情况。

一些概念
  1. 我们把多个进程(执行流)看到的公共的一份资源叫临界资源
  2. 我们把自己的进程访问临界资源的代码叫做临界区
  3. 为了更好地进行临界区的保护,可以让多执行流在任何一个时刻都只能有一个进程进入临界区 也就是互斥
  4. 多个执行流互相运行的时候互相干扰,主要是我们不加保护的访问了同样的资源(临界资源),在非临界区多个执行流是互相不能影响的
  5. 原子性:要么不做,要么做完,没有中间状态就称之为原子性

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

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

相关文章

kafka入门03——简单实战

目录 安装Java 安装Zookeeper 安装Kafka 生产与消费 主要是记录下Kafka的安装配置过程&#xff0c;前置条件需要安装jdk和zookeeper。 安装Java 1.Oracle官网下载对应jdk安装包 官网地址&#xff1a;Java Downloads | Oracle 好人分享了下载需要的oracle账号&#xff0c…

linux基本指令(Chapter 2)

linux基本指令(二) 1.date指令 语法:date [option] [format] date命令的作用是显示时间:使用date %Y:%m:%d可以指定格式显示时间.例如:(注意这里的和%之后不要有任何间隔)并且这里是加号,并不是减号 1.在显示方面&#xff0c;使用者可以设定欲显示的格式&#xff0c;格式设…

NeRF综述

文章目录 《Nerf: Neural radiance field in 3d vision, a comprehensive review 》一、数据集&#xff1a;二、基于方法的分类&#xff08;Method-based Taxonomy&#xff09;三、基于应用的分类&#xff08;Application-based Taxonomy&#xff09;四、未来展望 《Nerf: Neur…

盘点网安最好入手的10大岗位,最高月薪30K!

前段时间&#xff0c;知名机构麦可思研究院发布了《2022年中国本科生就业报告》&#xff0c;其中详细列出近五年的本科绿牌专业&#xff0c;信息安全位列第一。 对于网络安全的发展与就业前景已经说过很多&#xff0c;它是收入较高的岗位之一&#xff0c;在转行领域也占据着热…

番外8.1 Task

---------系统中有两个账号&#xff0c;分别是alex与arod&#xff0c;这两个账号除了支持自己的组&#xff0c;还共同支持一个名为project的组。如这两个账号需要共同拥有/srv/ahome/目录的开发权&#xff0c;且该目录不允许其他账号进入查阅&#xff0c;请问该目录的权限应如何…

动手学深度学习—含并行连结的网络GoogLeNet(代码详解)

目录 1. Inception块3. GoogLeNet模型3. 训练模型 GoogLeNet吸收了NiN中串联网络的思想&#xff0c;并在此基础上做了改进&#xff0c;并且在2014年的ImageNet图像识别挑战赛中获得了不错的效果。 1. Inception块 GoogLeNet论文解决了多大的卷积核最合适的问题。 Inception块…

yolo数据增强,同时旋转txt标签文件

github https://github.com/vkdx/vkdx_cnn-.git YOLO格式txt文件分析 标注好的txt文件中有对应每个标注框的信息,从左到有分别是&#xff1a; class:类别 x_center&#xff1a;标注框中心相对于图像的x坐标 y_center&#xff1a;标注框中心相对于图像的y坐标 w&#xff1a;标…

Day09字符流缓冲流序列化流IO框架

字符流 FileReader&#xff08;文件字符输入流&#xff09; 使用文件字符输入流&#xff0c;有啥好处&#xff1f; 读取中文不会出现乱码问题 FileWriter(文件字符输出流) 利用字符流将一个文本文件复制到E:盘下&#xff0c;例如&#xff1a;D:/1.txt复制到E:/2.txt 请使用…

C语言每日一题(17)数组匹配

牛客网 BC156 牛牛的数组匹配 题目描述 描述 牛牛刚学会数组不久&#xff0c;他拿到两个数组 a 和 b&#xff0c;询问 b 的哪一段连续子数组之和与数组 a 之和最接近。 如果有多个子数组之和同样接近&#xff0c;输出起始点最靠左的数组。 输入描述&#xff1a; 第一行输…

如何在不损失质量的情况下调整图像大小

如何在不损失质量的情况下调整图像大小 如果您在线工作&#xff0c;就会知道图像质量对于呈现干净专业的外观有多么重要。 库存图像和免版税图像很容易找到&#xff0c;但是如何在不损失质量的情况下调整图像大小以使其适合您的目的&#xff1f; 无论您是想将图片用于博客文…

【Java变量】 局部变量、成员变量(类变量,实例变量)、方法参数传递机制

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 变量 1. 局部变量与成员变量的区别&#xff1a…

小白必看,手把手教你安装Python

目录 一&#xff0c;Python介绍 二&#xff0c;安装 Python 三&#xff0c;各种疑难杂症&#xff1a; 一&#xff0c;Python介绍 Python 是这两年来比较流行的一门编程语言&#xff0c;主要卖点是其相对简单的语法以及丰富的第三方库&#xff0c;下面我来带大家安装、配置 P…

【七】SpringBoot为什么可以打成 jar包启动

SpringBoot为什么可以打成 jar包启动 简介&#xff1a;庆幸的是夜跑的习惯一直都在坚持&#xff0c;正如现在坚持写博客一样。最开始刚接触springboot的时候就觉得很神奇&#xff0c;当时也去研究了一番&#xff0c;今晚夜跑又想起来了这茬事&#xff0c;于是想着应该可以记录一…

Redis数据类型——list类型介绍及基本操作

1.list类型介绍 redis中的list就是一个双向链表的结构 2.list类型数据基本操作

己知一棵有 2011 个结点的树,其叶结点个数为 116,该树对应的二叉树无右孩子的结点个数是

前言 树转二叉树的规则&#xff1a;每个结点左指针指向它的第一个孩子&#xff0c;右指针指向它在树中相邻的右兄弟&#xff0c;即“左孩子右兄弟“。 拓展&#xff1a;树中一个叶子节点在转化为二叉树的时候&#xff0c;如果它有右兄弟&#xff0c;那么它右指针会指向其兄弟节…

【软件安装】Linux系统中安装MySQL数据库服务

这篇文章&#xff0c;主要介绍如何在Linux系统中安装MySQL数据库服务。 目录 一、Linux安装MySQL 1.1、下载MySQL安装包 1.2、解压MySQL安装包 1.3、更改存放目录 1.4、创建用户组和用户 1.5、创建数据目录data 1.6、创建my.cnf配置文件 1.7、初始化数据库 1.8、添加m…

报告从root到sink的clock tree物理长度的脚本

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 距离clock root物理距离最远的sink未必是latency最大的,但是往往clock path的长度受其影响,比如一些寄存器放在距离clock root很远的channel里,与其balance的reg就需要垫 delay detour buffer,即…

【Maven教程】(九):使用 Maven 进行测试 ~

目录 1️⃣ account-captcha 1.1 account-captcha 1.2 account-captcha 的主代码 1.3 account-captcha的测试代码 2️⃣ maven-surefire-plugin 简介 3️⃣ 跳过测试 4️⃣ 动态指定要运行的测试用例 5️⃣ 包含与排除测试用例 6️⃣ 测试报告 6.1基本的测试报告 6.…

鸿蒙应用开发之环境搭建

一、环境搭建 正所谓“工欲善其事&#xff0c;必先利其器”。在正式学习一门课程之前&#xff0c;我们首先需要做的就是搭建开发环境。首先&#xff0c;我们需要下载DevEco Studio&#xff0c;DevEco Studio支持Windows系统和macOS系统&#xff0c;在开发HarmonyOS应用/服务前…

力扣每日一题64:最小路径和

题目描述&#xff1a; 给定一个包含非负整数的 m x n 网格 grid &#xff0c;请找出一条从左上角到右下角的路径&#xff0c;使得路径上的数字总和为最小。 说明&#xff1a;每次只能向下或者向右移动一步。 示例 1&#xff1a; 输入&#xff1a;grid [[1,3,1],[1,5,1],[4,2…