进程间通信(上)

news2024/9/27 5:45:39

进程间通信(上)

  • 背景
  • 进程间通信目的
  • 进程间通信发展
  • 进程间通信分类
  • 管道
    • 什么是管道
    • 匿名管道
      • 实例代码
        • 简单的匿名管道实现
        • 一个父进程控制单个子进程完成指定任务
        • 父进程控制一批子进程完成任务(进程池)
      • 用fork来共享管道
      • 站在文件描述符角度-深度理解管道
      • 站在内核角度-管道本质
      • 理解管道操作 -- |
    • 管道读写规则
    • 管道的特点
    • 命名管道
      • 原理
      • 创建一个命名管道
      • 命名管道具体示例
        • 命令行创建
        • 代码示例

背景

1、进程是具有独立性的,所以进程间想要交互数据,成本会非常高

2、为什么要进行进程间通信?有的时候需要多进程协同处理一件事情。

进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信发展

  • 管道
  • System V进程间通信
  • POSIX进程间通信

进程间通信分类

管道

  • 匿名管道
  • pipe 命名管道

System V IPC(用的不多,更多的是进行本地通信)

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC(用的较多,也可以用来进行网络通信)

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

注意:System V IPC和POSIX IPC是两套标准(IPC是通信的简称)。

管道

什么是管道

  • 管道是Unix中最古老的进程间通信的形式。

  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

image-20221031150427050

匿名管道

image-20221031170631490

里面封装了两次open,第一次是以读方式打开,返回值写在fd[0]中,也就是打开文件的fd,第二次是以写方式打开,返回值写在fd[1]中,也就是打开文件的fd。同时通过上面的联合体标定它是一个管道文件。

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

image-20221031155504450

实例代码

简单的匿名管道实现

#include<iostream>
#include<cstdio>
#include<fcntl.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string>
#include<cstring>
#include<sys/wait.h>
using namespace std;
int main()
{
  //1.创建管道
  int pipefd[2] = {0};
  if(pipe(pipefd) != 0)
  {
    cerr << "pipe error" << endl;
    return 1;
  }
  //2.创建子进程
  pid_t id = fork();
  if(id < 0)
  {
    cerr << "fork error" << endl;
    return 2;
  }
  else if(id == 0)//子进程
  {
    //子进程来读取,关闭写端
    close(pipefd[1]);
#define MAX_NUM 1024
    char buffer[MAX_NUM];
    while(true)
    {
      memset(buffer, 0, sizeof(buffer));
      ssize_t ret = read(pipefd[0], buffer, sizeof(buffer) - 1);
      if(ret > 0)
      {
        //读取成功,可以进行写入
        buffer[ret] = '\0';
        cout << "子进程受到消息了,消息内容:" << buffer << endl;
      }
      else if(ret == 0) 
      {
        sleep(1);//此处是为了稍微等一下父进程
        cout << "父进程写完了,我也退出了!" << endl;
        break;
      }
      else 
      {
        //Do nothing
      }
    }
    close(pipefd[0]);
    exit(0);
  }
  else//父进程
  {
    //父进程来写入,关闭读端
    close(pipefd[0]);
    const string msg = "你好子进程,我是父进程!这次发送的信息编号是: ";
    int cnt = 0;
    while(cnt < 5)
    {
      char sendBuffer[1024];
      sprintf(sendBuffer, "%s:%d", msg.c_str(), cnt);
      write(pipefd[1], sendBuffer,strlen(sendBuffer));
      sleep(1);//为了看现象明显设计的
      cnt++;
    }
    close(pipefd[1]);
    cout << "父进程写完了" << endl;
  }
  pid_t res = waitpid(id, nullptr, 0);
  if(res > 0)
  {
    cout << "等待子进程成功!" << endl;
  }
  return 0;
}

问:父进程关闭写端了,子进程是如何知道父进程关闭写端的?

答:通过引用计数知道的,file结构体中,有类似引用的变量记录了有几个指针指向该文件。当引用计数为1了,说明此时就只有一个进程指向该文件了。此时子进程读完就不再有进程指向该文件了。

问:父进程每隔一秒写一次,为什么子进程也是一秒读一次呢?

答:当父进程在写入数据的时候,子进程在等(阻塞等待:将当前进程放在等待队列中(管道资源的等待队列中))!所以,父进程写入之后,子进程才能read(会返回)到数据,子进程打印读取数据要以父进程的节奏为主。所以父进程和子进程在读写的时候,是有一定的顺序性的(pipe内部自带访问控制(同步和互斥机制))。(父子进程在各自printf的时候(向显示器写入文件),并没有顺序,谁快谁先写,缺乏访问控制)。

管道内部,没有数据,reader就必须阻塞等待(等管道有数据);管道内部如果被写满了,writer就必须阻塞等待(等数据被读走)。

一个父进程控制单个子进程完成指定任务

代码:

#include<iostream>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<ctime>
#include<string>
#include<vector>
#include<unordered_map>
#include<cassert>
#include<cstdlib>

using namespace std;

typedef void(*functor)();
vector<functor> functors;//方法集合
//for debug
unordered_map<uint32_t, string> info;
void f1()
{
  cout << "这是一个处理日志的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl;;
}
void f2()
{
  cout << "这是一个备份数据的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl;;
}
void f3()
{
  cout << "这是一个处理网络请求的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl;;
}
void loadFunctor()
{
  info.insert({functors.size(), "处理日志的任务"});
  functors.push_back(f1);
  info.insert({functors.size(), "处理备份数据的任务"});
  functors.push_back(f2);
  info.insert({functors.size(), "处理网络请求的任务"});
  functors.push_back(f3);
}

int main()
{
  //0.加载任务列表
  loadFunctor();
  //1.创建管道
  int pipefd[2] = {0};
  if(pipe(pipefd) != 0)
  {
    cerr << "pipe error" << endl;
    return 1;
  }
  //2.创建子进程
  pid_t id = fork();
  if(id < 0)
  {
    cerr << "fork error" << endl;
    return 2;
  }
  else if(id == 0)
  {
    //3.关闭不需要的文件
    close(pipefd[1]);
    //child:read
    //4.业务处理
    while(true)
    {
      uint32_t operatorType = 0;
      //如果有数据就读取,如果没有数据就阻塞等待,等待任务的到来
      ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32_t));
      if(s == 0)
      {
        cout << "读取数据结束!退出!" << endl;
        break;
      }
      assert(s == sizeof(uint32_t));
      //assert是断言,是编译有效debug模式
      //release模式,断言就没有了
      //如果断言没有了,那么s变量就是只被定义,没有被使用,在release模式中,就会有warning存在
      (void)s;
      if(operatorType < functors.size())
      {
        functors[operatorType]();
      }
      else 
      {
        cerr << "bug?operatorType = " << operatorType << endl;
      }
    }
    close(pipefd[0]);
    exit(0);
  }
  else if(id > 0)
  {
    srand((long long)time(nullptr));
    //3.关闭不需要的文件
    close(pipefd[0]);
    //parant:write
    //4.业务生成
    int num = functors.size();
    int cnt = 10;
    while(cnt--)
    {
      //5.形成任务码
      uint32_t commandCode = rand() % num;
      cout << "父进程指派任务完成,任务是:" << info[commandCode] << ", 任务的编号是:"<< cnt << endl;
      //向指定的进程下达执行任务的操作
      
      write(pipefd[1], &commandCode, sizeof(uint32_t));
      sleep(1);
    }
    close(pipefd[1]);
    pid_t res = waitpid(id, nullptr, 0);
    if(res)
    {
      cout << "wait success" << endl;
    }
  }

  return 0;
}

父进程控制一批子进程完成任务(进程池)

代码:

#include<iostream>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<ctime>
#include<string>
#include<vector>
#include<unordered_map>
#include<cassert>
#include<cstdlib>

using namespace std;

typedef void(*functor)();
vector<functor> functors;//方法集合
//for debug
unordered_map<uint32_t, string> info;
void f1()
{
  cout << "这是一个处理日志的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl << endl;;
}
void f2()
{
  cout << "这是一个备份数据的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl <<  endl;;
}
void f3()
{
  cout << "这是一个处理网络请求的任务, 执行的进程id:" << getpid() << "执行时间是" << time(nullptr) << endl << endl;;
}
void loadFunctor()
{
  info.insert({functors.size(), "处理日志的任务"});
  functors.push_back(f1);
  info.insert({functors.size(), "处理备份数据的任务"});
  functors.push_back(f2);
  info.insert({functors.size(), "处理网络请求的任务"});
  functors.push_back(f3);
}
//int32_t:进程pid  int32_t:该进程对应的管道写端fd
typedef pair<int32_t, int32_t> elem;
int processNum = 5;//创建子进程的个数
void work(int blockFd)
{ 
  cout << "进程:" << getpid() << "开始工作" << endl;
  //子进程核心工作的代码
  while(true)
  {
    //a.阻塞等待 b.获取任务信息
    uint32_t operatorCode = 0;
    ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));
    if(s == 0)
    {
      break;
    }
    assert(s == sizeof(uint32_t));
    (void)s;
    //c.处理任务
    if(operatorCode < functors.size())
    {
      functors[operatorCode]();
    }
  }
  cout << "进程:" << getpid() << "结束工作" << endl;
}
//[子进程的pid, 子进程的管道fd]
void BalanceSendTask(vector<elem>& processFds)
{
  srand((long long)time(nullptr));
  int cnt = 10;//cnt是要分配任务的数目
  while(cnt != 0)
  {
    sleep(1);
    //选择一个进程
    int pick = rand() % processFds.size();
    //选择一个任务
    int task = rand() % functors.size();
    //把任务给一个指定的进程
    write(processFds[pick].second, &task, sizeof(int));
    //打印对应的提示信息
    cout << "父进程指派任务"<< info[task] << "给进程:"  << processFds[pick].first << "编号:" << pick << endl;
    cnt--;
  }
}
int main()
{
  loadFunctor();
  vector<elem> assignMap;
  //创建processNum个进程
  for(int i = 0; i < processNum; i++)
  {
    //定义保存管道fd的对象
    int pipefd[2] = {0};
    //创建管道
    pipe(pipefd);
    //创建子进程
    pid_t id = fork();
    if(id == 0)
    {
      //子进程读取
      close(pipefd[1]);
      //子进程执行
      work(pipefd[0]);
      close(pipefd[0]);
      exit(0);
    }
    //父进程做的事情   
    close(pipefd[0]);
    elem e(id, pipefd[1]);
    assignMap.push_back(e);
  }
  cout << "creat all process success!" << endl;
  //父进程派发任务
  BalanceSendTask(assignMap);
  
  //回收资源
  for(int i = 0; i < processNum; i++)
  {
    close(assignMap[i].second);
  }
  for(int i = 0; i < processNum; i++)
  {
    if(waitpid(assignMap[i].first, nullptr, 0) > 0)
    {
      cout << "wait for" << assignMap[i].first << "success!"  << "number:" << i << endl;
    }
  }
  return 0;
}

用fork来共享管道

image-20221031161452915

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

image-20221031164213553

问:为什么父进程要分别打开读和写?

答:为了让子进程继承,让子进程不必再打开了。

问:为什么父子要关闭对应的读和写?

答:因为管道必须是单向通信的,一端是读端,另已端必须是写端。

问:谁决定父子关闭读端还是写端?

答:由需求决定。

站在内核角度-管道本质

image-20221101192704676

理解管道操作 – |

注意:|操作的本质就是匿名管道

image-20221101193113741

sleep 1000 | sleep 100

这两个进程(sleep 1000和sleep 100)的关系是什么呢?两个进程的ppid是一样的,即有同样的父进程。

以下面的命令进行举例:

cat pipe.cc | wc -l 

父进程fork两个子进程即cat pipe.ccwc -l,父进程在创建进程的同时,创建了一条匿名管道,两个进程通过该匿名管道来进行通信,cat pipe.cc和wc -l两个进程分别关闭读写端,父进程关闭这个进程的读写端,cat进程进行输出重定向,wc进程进行输入重定向。

管道读写规则

  • 当没有数据可读时:
    • 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将不再保证写入的原子性。

管道的特点

  1. 管道只能用来进行具有血缘关系的进程之间进行进程间通信,常用于父子间通信或者兄弟间通信。(只属于匿名管道)
  2. 管道只能单向通信(内核实现决定的),半双工的一种特殊情况
  3. 管道自带同步机制(内核会对管道操作进行同步与互斥)(pipe满,writer等,pipe空,reader等)
  4. 管道是面向字节流的,先写的字符,一定是先被读取的,没有格式边界,需要用户来定义区分内容的边界
  5. 管道的生命周期跟随进程 – 管道是文件 – 进程退出了,曾经打开是文件引用计数到达0就会自动退出

命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

原理

image-20221101210357942

和匿名管道的区别:

匿名管道:子进程继承父进程。

命名管道:通过打开同一个fifo文件,进行信息的交互(路径具有唯一性)

注意:我们使用的命名管道,更多的时候是作为一种标定的作用,内存中的管道文件中的数据不会刷新到磁盘中,即使在进行通信的时候,命名管道的大小也始终是0个字节

创建一个命名管道

  • 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

    $ mkfifo filename
    
  • 命名管道也可以从程序里创建,相关函数有:

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

命名管道具体示例

命令行创建

简单使用:

image-20221101202732807

image-20221101202801137

代码示例

创建管道文件:

image-20221101202319701

删除管道文件:

image-20221102101704696

代码示例:

clientFifo.cpp文件:

#include"comm.h"
using namespace std;
int main()
{
  int pipeFd = open(IPC_PATH, O_WRONLY);
  if(pipeFd < 0)
  {
    cerr << "open error" << endl;
    return 1;
  }
#define NUM 1024
  char line[NUM];
  while(true)
  {
    printf("请输入你的消息#");
    fflush(stdout);
    memset(line, 0, sizeof(line));
    if(fgets(line, sizeof(line), stdin) != nullptr)
    {
      line[strlen(line) - 1] = '\0';
      write(pipeFd, line, strlen(line));//12345\n\0
    }
    else 
    {
      break;
    }

  }
  cout << "退出客户端" << endl;
  close(pipeFd);
  return 0;
}

serverFifo.cpp文件:

//写入
#include"comm.h"
using namespace std;
int main()
{
  if(mkfifo(IPC_PATH, 0666) != 0)//创建管道文件
  {
    cerr << "mkfifo error" << endl;
    return 1;
  }
  int pipeFd = open(IPC_PATH, O_RDONLY);
  if(pipeFd < 0)
  {
    cerr << "open error" << endl;
    return 2;
  }
  //正常的通信过程
#define NUM 1024
  char buffer[NUM];
  while(true)
  {
    ssize_t s = read(pipeFd, buffer, sizeof(buffer) - 1);
    if(s > 0)
    {
      buffer[s] = '\0';
      cout << "客户端->服务器#" << buffer << endl;
    }
    else if(s == 0)
    {
      cout << "客户退出了,我也退出了!" << endl;
      break;
    }
    else
    {
      //do nothing
      cerr << "read" << strerror(errno) << endl;
    }
  }
  close(pipeFd);
  cout << "服务端退出了" << endl;
  unlink(IPC_PATH);//s
  return 0;
}

comm.h文件:

#pragma once
#include<cstdio>
#include<iostream>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<cstring>
#include<cerrno>
#define IPC_PATH "./.fifo"

makefile文件:

.PHONY:all
all:clientFifo serverFifo
	
clientFifo:clientFifo.cpp
	g++  -o $@ $^ -std=c++11
serverFifo:serverFifo.cpp
	g++  -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -rf clientFifo serverFifo .fifo

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

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

相关文章

C++:类和对象(中)

文章目录1 类的6个默认成员函数2 构造函数2.1 概念2.2 特性3 析构函数3.1 概念3.2 特性4 拷贝构造函数4.1 概念4.2 特性5 赋值运算符重载5.1 运算符重载5.2 赋值运算符重载5.3 前置重载和后置重载6 日期类的实现7 const成员8 取地址及const取地址操作符重载1 类的6个默认成员函…

【C++初阶】十三、模板进阶(总)|非类型模板参数|模板的特化|模板分离编译|模板总结(优缺点)

目录 一、非类型模板参数 二、模板的特化 2.1 模板特化概念 2.2 函数模板特化 2.3 类模板特化 2.3.1 全特化 2.3.2 偏特化 三、模板分离编译 四、模板总结&#xff08;优缺点&#xff09; 前言&#xff1a;之前模板初阶并没有把 C模板讲完&#xff0c;因为当时没有接触…

Java——聊聊JUC中的原子变量类

文章目录&#xff1a; 1.什么是原子变量类&#xff1f; 2.AtomicInteger&#xff08;基本类型原子变量类&#xff09; 3.AtomicIntegerArray&#xff08;数组类型原子变量类&#xff09; 4.AtomicMarkableReference&#xff08;引用类型原子变量类&#xff09; 5.AtomicInteger…

二叉树OJ题(上)

✅每日一练&#xff1a;100. 相同的树 - 力扣&#xff08;LeetCode&#xff09; 题目的意思是俩棵树的结构不仅要相同&#xff0c;而且每个节点的值还要相同&#xff0c;如果满足上面2个条件&#xff0c;则成立&#xff01; 解题思路&#xff1a; 从三个方面去考虑&#xff1…

分布式之分布式事务V2

写在前面 本文一起来看下分布式环境下的事务问题&#xff0c;即我们经常听到的分布式事务问题。想要解决分布式事务问题&#xff0c;需要使用到分布式事务相关的协议&#xff0c;主要有2PC即两阶段提交协议&#xff0c;TCC&#xff08;try-confirm-cancel&#xff09;&#xf…

FPGA产业发展现状及人才培养研究报告

文章目录一、FPGA赋能智能时代二、FPGA市场现状及挑战2.1 FPGA市场发展现状2.2 FPGA主要应用场景2.3 人才问题成为FPGA发展的桎梏三、FPGA人才需求与人才培养3.1 FPGA人才需求特征3.2 FPGA人才培养现状3.2.1 培养主体3.2.2 培养机制3.2.3 培养人才的目的和宗旨3.2.4 FPGA人才培…

【C++】六个默认成员函数——取地址重载,const成员函数

&#x1f345; 初始化和清理 拷贝复制 目录 ☃️1.取地址重载 ☃️2.const取地址操作符重载 这两个运算符一般不需要重载&#xff0c;使用编译器生成的默认取地址的重载即可&#xff0c;只有特殊情况&#xff0c;才需要重载&#xff0c;比如想让别人获取到指定的内容&#xf…

计算机网络3:HTTP1.0和HTTP1.1的区别

目录1. HTTP是什么2.HTTP1.0和HTTP1.1的区别3.名词解释&#xff08;1&#xff09;If-Modified-Since&#xff08;IMS&#xff09;、Expires&#xff08;2&#xff09;If-None-Match&#xff0c;Etag&#xff08;3&#xff09;If-Unmodified-Since1. HTTP是什么 超文本传输协议…

2023全新SF授权系统源码 V3.7全开源无加密版本,亲测可用

2023全新SF授权系统源码 V3.7全开源无加密版本。网站搭建很简单&#xff0c;大致看来一下应该域名解析后上传源码解压&#xff0c;访问域名/install就能直接安装。 程序功能简介: 1.盗版入库(26种) 2.快捷登录 3.采用layuiadmin框架 4.易支付认证功能 5.程序自带商城系统…

SSO(单点登陆)

Single Sign On 一处登陆、处处可用 0、前置概念&#xff1a; 1&#xff09;、单点登录业务介绍 早期单一服务器&#xff0c;用户认证。 缺点&#xff1a;单点性能压力&#xff0c;无法扩展 分布式&#xff0c; SSO(single sign on)模式 解决 &#xff1a; 用户身份信息独…

微信小程序Springboot vue停车场车位管理系统

系统分为用户和管理员两个角色 用户的主要功能有&#xff1a; 1.用户注册和登陆系统 2.用户查看系统的公告信息 3.用户查看车位信息&#xff0c;在线预约车位 4.用户交流论坛&#xff0c;发布交流信息&#xff0c;在线评论 5.用户查看地图信息&#xff0c;在线导航 6.用户查看个…

Win11自定义电脑右下角时间显示格式

Win11自定义电脑右下角时间显示格式 一、进入附加设置菜单 1、进入控制面板&#xff0c;选择日期和时间 2、选择修改日期和时间 3、选择修改日历设置 4、选择附加设置 二、自定义时间显示出秒 1、在选项卡中&#xff0c;选时间选项卡 2、在Short time的输入框中输入H:m…

家政服务小程序实战教程04-页面传参及表单容器

我们在上一篇已经介绍了在生命周期函数中预加载会员信息&#xff0c;首次使用小程序的用户需要进行注册&#xff0c;注册的时候需要选择对应的角色&#xff0c;本篇我们就介绍会员注册的功能。 01 创建页面 会员注册&#xff0c;我们分两个页面&#xff0c;一个是角色选择页面…

VSCode Markdown写作引入符合规范的参考文献

Markdown可以用来写论文&#xff0c;写论文的时候无一例外要用到参考文献&#xff0c;今天来谈谈怎么自动生成参考文献。之前讲了怎么导出的pdf&#xff0c;文章在这里 VSCode vscode-pandoc插件将中文Markdown转换为好看的pdf文档&#xff08;使用eisvogel模板&#xff09; …

CMake中target_precompile_headers的使用

CMake中的target_precompile_headers命令用于添加要预编译的头文件列表&#xff0c;其格式如下&#xff1a; target_precompile_headers(<target><INTERFACE|PUBLIC|PRIVATE> [header1...][<INTERFACE|PUBLIC|PRIVATE> [header2...] ...]) # 1 target_preco…

select 与 where、group by、order by、limit 子句执行优先级比较

当 select 和 其他三种语句的一者或者多者同时出现时&#xff0c;他们之间是存在执行先后顺序的。 他们的优先级顺序是&#xff1a;where > group by > select > order by > limit 目录 1、select 与 where 2、group by 与 where 、select 2、select 与 order…

【Call for papers】CRYPTO-2023(CCF-A/网络与信息安全/2023年2月16日截稿)

Crypto 2023 will take place in Santa Barbara, USA on August 19-24, 2023. Crypto 2023 is organized by the International Association for Cryptologic Research (IACR). The proceedings will be published by Springer in the LNCS series. 文章目录1.会议信息2.时间节…

C++定位new用法及注意事项

使用定位new创建对象&#xff0c;显式调用析构函数是必须的&#xff0c;这是析构函数必须被显式调用的少数情形之一&#xff01;&#xff0c; 另有一点&#xff01;&#xff01;&#xff01;析构函数的调用必须与对象的构造顺序相反&#xff01;切记&#xff01;&#xff01;&a…

分步骤详解随机生成一个登录验证码的算法,最后给出完整代码

需要安装第三方模块pillow import randomfrom PIL import Image, ImageDraw, ImageFont步骤一&#xff1a;编写一个生成随机颜色的函数 def get_random_color():return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)步骤二&#xff1a;在面板里放…

31 岁生日快乐,Linux!

Linux 迎来了 31 岁生日&#xff0c;所以和我一起庆祝 Linux 的 31 岁生日吧&#xff0c;喝上一杯好香槟和一个美味的蛋糕&#xff01;虽然有些人不承认 8 月 25 日是 Linux 的生日&#xff0c;但我知道。1991 年 8 月 25 日&#xff0c;21 岁的芬兰学生 Linus Benedict Torval…