【Linux】进程间通信 —— 管道与 System V 版本通信方式

news2024/9/17 9:00:02

目录

为什么有进程间通信?进程间通信的目的是什么?

管道

匿名管道

父子进程共享管道

命名管道

共享内存

概念

原理

共享内存和内存映射(文件映射)的区别

使用

消息队列

概念

使用

信号量

概念

使用

IPCS 命令

System V 版本进程通信内核表示


为什么有进程间通信?进程间通信的目的是什么?

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

管道

什么是管道?管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

管道怎么实现的?内核环形缓冲区

优点:实现简单,管道通信不需要像消息队列、共享内存等进程通信方式那样对系统资源进行复杂管理,实现起来较为简单方便。

缺点:

  1. 通信方式效率低,不适合进程间频繁地交换数据。
  2. 数据是无格式的流且大小受限,Ubuntu 20.04下管道大小为 65536 B = 64 KB
  3. 通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道。

匿名管道

特点:

  1. 自带同步机制。
  2. 匿名管道是只能用于存在父子关系的进程间通信。
  3. pipe 是面向字节流的。
  4. 没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中。
  5. 生命周期随着进程创建而建立,随着进程终止而消失。父子进程退出,管道自动释放,因为管道文件的生命周期是随进程的
  6. 管道只能单向数据通信的,这就意味着管道是半双工的一种特殊情况。

匿名管道使用的四种情况:

  1. 管道内部没有数据 && 子进程(写端)不关闭自己的写端文件fd,读端(父进程)就要阻塞等待,直到pipe有数据。
  2. 管道内部被写满 && 父进程(读端)不关闭自己的读端文件fd,写端(子进程)写满之后就要阻塞等待。这个大小在Ubuntu 20.04为 65536 B = 64 KB。
  3. 写端不写了 && 关闭了pipe,读端会将pipe中的数据读完,最后就会读取到返回值0,表示读结束。
  4. 读端不读了 && 关闭了pipe,写端再写就无意义了,OS会直接终止写入的进程(子进程),通过信号13) SIGPIPE杀掉进程。

此外,pipe 内部定义了 PIPE_BUF = 4 KB,PIPE_BUF 的定义见 <limits.h>

  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性;
  • 当要写入的数据量大于PIPE_BUF时,linux将不保证写入的原子性。

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

父子进程共享管道

使用 pipe 创建匿名管道,然后父进程调用 close(fd[1]) 关闭写段,子进程调用 close(fd[0]) 关闭读端,就可以实现父子进程通信。

示例代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

void writer(int wfd)
{
    const char *str = "hello father, I am child";
    char buffer[128];
    int cnt = 0;
    pid_t pid = getpid();
    while(1)
    {
        snprintf(buffer, sizeof(buffer), "message: %s, pid: %d, count: %d\n", str, pid, cnt);
        write(wfd, buffer, strlen(buffer));
        cnt++;
        sleep(1);
    }
}

void reader(int rfd)
{
    char buffer[1024];
    while(1)
    {
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        (void)n;
        printf("father get a message: %s", buffer);
    }
}

int main()
{
    // 1. 
    int pipefd[2];
    int n = pipe(pipefd);
    if(n < 0) return 1;
    printf("pipefd[0]: %d, pipefd[1]: %d\n", pipefd[0]/*read*/, pipefd[1]/*write*/);

    // 2. 
    pid_t id = fork();
    if(id == 0)
    {
        //child: w
        close(pipefd[0]);
        writer(pipefd[1]);
        exit(0);
    }
    // father: r
    close(pipefd[1]);
    reader(pipefd[0]);
    wait(NULL);
    return 0;
}

命名管道

在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。

调用接口

#include <sys/types.h>

#include <sys/stat.h>

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

#include <fcntl.h>           /* Definition of AT_* constants */

#include <sys/stat.h>

int mkfifoat(int dirfd, const char *pathname, mode_t mode);

#include <unistd.h>

int unlink(const char *pathname);

共享内存

概念

共享内存区是最快的 IPC 形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。

共享内存可以存在很多个,OS必须对这些共享内存进行管理 --- 先描述,再组织!

共享内存数据结构如下:

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 */
};

原理

  1. 拿出一块虚拟地址空间来,映射到相同的物理内存中。

  2. 解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问。

  3. 带来新的问题,多进程竞争同个共享资源会造成数据的错乱。解决方式:1. 信号量 2. 条件变量 + 锁。

共享内存和内存映射(文件映射)的区别

共享内存:利用共享内存完成进程间通信,两个进程通过虚拟内存到用户级页表,然后通过用户级页表最后访问同一块物理内存,实现进程间通信。

mmap:

  • mmap 是在磁盘上建立一个文件,每个进程地址空间中开辟出一块空间进行映射。而 shm 共享内存,每个进程最终会映射到同一块物理内存
  • mmap 会把数据同步到文件中(会有缓存机制),shm不会把数据写入文件,这意味着 mmap 重启不会丢失共享数据。
  • mmap 共享数据需要通过文件,io 效率要比 shm 的内存读写肯定要低很多。
  • 内存空间比磁盘空间小很多,所以使用 mmap 可以共享的数据比 shm 大很多。

关于 mmap 以及 malloc / free 可查看以下文章:【C语言】一文详解 malloc / free 分配内存和释放内存相关问题-CSDN博客

使用

头文件

#include <sys/ipc.h>
#include <sys/shm.h>

shmget

功能:用来创建共享内存
原型
        int shmget(key_t key, size_t size, int shmflg);
参数
        key:这个共享内存段名字
        size:共享内存大小
        shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

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

消息队列

概念

实际上是保存在内核的消息链表,消息队列的消息体是可以用户自定义的数据类型。

优点

  1. 可以频繁通信了
  2. 可以独立于读写进程存在,避免了 FIFO 中同步管道打开和关闭可能产生的困难
  3. 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法
  4. 读进程可以根据消息消息类型有选择的接受消息,而不是像 FIFO 那样默认接受

缺点

  1. 通信不及时,每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。
  2. 消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。

消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在。

使用

头文件

#include <sys/types.h>
#include <sys/ipc.h>

#include <sys/msg.h>

msgget

功能:获取消息队列标识符

原型

         int msgget(key_t key, int msgflg);

msgctl

功能:用于控制消息队列

原型

       int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msgsnd

功能:用于发送数据给消息队列

原型

       int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msgrcv

功能:用于从消息队列接收数据       

原型

        ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

信号量

概念

对于共享资源进行保护,是一个多执行流场景下,一个比较常见和重要的话题。

互斥:在访问一部分共享资源的时候,任何时刻只有一个进程访问。

同步:访问资源在安全的前提下,具有一定的顺序性。

临界资源:被保护起来的,任何时刻只允许一个进程进行访问的公共资源。

临界区:访问临界资源的代码。

信号量本质上是一个描述临界资源数量的计数器,对公共局部临界资源的预定机制,用来保护临界资源

假设用 count 描述临界资源的数量。

可以将信号量申请看作预定资源,if (count > 0) count--; else wait;

将信号量释放看作释放资源,此临界资源可以被其他进程申请,count++;

但是,用 int 类型定义 count 不能实现信号量的效果。

原因:

1. 无法在进程间共享,对 count 的操作会触发写时拷贝,于是就要让进程提前看到一部分资源 --- 信号量这种计数器资源。

2. count++,count-- 的操作不是原子的。

所有的资源要访问临界资源,都必须先申请信号量,那么所有进程都得先看到同一个信号量,同时,信号量本身就是共享资源!信号量的申请和释放就必须是原子性的。信号量申请是P操作,信号量释放是V操作,信号量的申请释放称为PV操作。

如果信号量初始值是1呢?代表将临界资源看作整体,来实现互斥,二元信号量就是一把锁。

使用

头文件

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

功能:创建信号量集标识符

int semget(key_t key, int nsems, int semflg);

功能:op是operation的缩写,对指定信号量进行操作

int semop(int semid, struct sembuf *sops, size_t nsops);
 

功能:对指定信号量进行控制

int semctl(int semid, int semnum, int cmd, ...);

该函数参数数量为 3 或 4,取决于 cmd 参数,第 4 个参数是一个 union 类型变量,如下图:

IPCS 命令

ipcs [options]

options:

-m

查看系统中的共享内存

-q

查看系统中的消息队列

-s

查看系统中的信号量

-a

查看当前使用的共享内存、消息队列及信号量所有信息

-p

得到与共享内存、消息队列相关进程之间的消息

-u

查看各个资源的使用总结信息

-l

查看各个资源的系统限制信息

ipcrm [options] <id>

options:

-m <shmid>

删除 shmid 对应的共享内存

-q <msgid>

删除 msgid 对应的消息队列

-s <semid>

删除 semid 对应的信号量

-a

删除所有进程间通信的资源

System V 版本进程通信内核表示

/* used by in-kernel data structures */
struct kern_ipc_perm
{
	spinlock_t	lock;
	int		deleted;
	key_t		key;
	uid_t		uid;
	gid_t		gid;
	uid_t		cuid;
	gid_t		cgid;
	mode_t		mode; 
	unsigned long	seq;
	void		*security;
};

struct ipc_id_ary {
	int size;
	struct kern_ipc_perm *p[0];
};

struct shmid_kernel /* private to the kernel */
{	
	struct kern_ipc_perm	shm_perm;
	struct file *		shm_file;
	int			id;
	unsigned long		shm_nattch;
	unsigned long		shm_segsz;
	time_t			shm_atim;
	time_t			shm_dtim;
	time_t			shm_ctim;
	pid_t			shm_cprid;
	pid_t			shm_lprid;
	struct user_struct	*mlock_user;
};

struct msg_queue {
	struct kern_ipc_perm q_perm;
	time_t q_stime;			/* last msgsnd time */
	time_t q_rtime;			/* last msgrcv time */
	time_t q_ctime;			/* last change time */
	unsigned long q_cbytes;		/* current number of bytes on queue */
	unsigned long q_qnum;		/* number of messages in queue */
	unsigned long q_qbytes;		/* max number of bytes on queue */
	pid_t q_lspid;			/* pid of last msgsnd */
	pid_t q_lrpid;			/* last receive pid */

	struct list_head q_messages;
	struct list_head q_receivers;
	struct list_head q_senders;
};

struct sem_array {
	struct kern_ipc_perm	sem_perm;	/* permissions .. see ipc.h */
	time_t			sem_otime;	/* last semop time */
	time_t			sem_ctime;	/* last change time */
	struct sem		*sem_base;	/* ptr to first semaphore in array */
	struct sem_queue	*sem_pending;	/* pending operations to be processed */
	struct sem_queue	**sem_pending_last; /* last pending operation */
	struct sem_undo		*undo;		/* undo requests on this array */
	unsigned long		sem_nsems;	/* no. of semaphores in array */
};

共享内存、消息队列、信号量的部分源码如上,可以看到:

共享内存、消息队列、信号量 --- 存在共性,这是操作系统故意为之的,用于 system V 进程间通信的,因为操作系统注定要对 IPC 资源进行管理,如何管理?先描述,再组织

内核中,所有描述管理 IPC 资源的结构体,第一个成员都一样 --- kern_ipc_perm,然后定义出了一个与文件描述符表毫无关系的 ipc_id_ary 结构体,里面包含申请到的 kern_ipc_perm 和 对应的个数,用于管理 IPC 资源,而这种设计方式与文件操作进行了隔离,有悖于 Linux 里的一切皆文件所以这种技术后来逐渐被替代了

这种设计方式也有优点,用C语言实现了高级语言里的多态!!

(shmid_kernel*)ipc_array[0]->shm_nattch;
(msg_queue*)ipc_array[1]->q_stime;
(sem_array*)ipc_array[2]->sem_pending;

那么我怎么知道 ipc_array 里 i 号下标的是什么资源呢?

#define IPC_SHM_TYPE 1 << 0
#define IPC_MSG_TYPE 1 << 1
#define IPC_SEM_TYPE 1 << 2

auto GetKernIpcPerm(kern_ipc_perm* p)
{
    if (p->mode & IPC_SHM_TYPE)
        return (shmid_kernel*) p;
    else if (p->mode & IPC_MSG_TYPE)
        return (msg_queue*) p;
    else if (p->mode & IPC_SEM_TYPE)
        return (sem_array*) p;
}

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

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

相关文章

【人工智能专栏】Cross Entropy 交叉熵损失解析

Cross Entropy 交叉熵 信息熵 在信息世界中我们所有的信息都可以抽象为“情况”,用二进制 bit 来表达,正因为每个 bit 都有 0 1 两种“情况”,所以 n n n 个 bit 可以编码 2 n 2^n 2

Java----代理

什么是代理&#xff1f; 在Java中&#xff0c;代理是一种用于创建一个或多个服务的中间层&#xff0c;它可以拦截并处理程序对实际服务对象的请求。代理模式是一种设计模式&#xff0c;属于结构型模式&#xff0c;它允许程序员在不修改实际对象代码的情况下&#xff0c;增强或控…

PHP反序列化漏洞从入门到深入8k图文介绍,以及phar伪协议的利用

文章参考&#xff1a;w肝了两天&#xff01;PHP反序列化漏洞从入门到深入8k图文介绍&#xff0c;以及phar伪协议的利用 前言 本文内容主要分为三个部分&#xff1a;原理详解、漏洞练习和防御方法。这是一篇针对PHP反序列化入门者的手把手教学文章&#xff0c;特别适合刚接触PH…

杭州等保测评的备案流程

杭州等级保护备案和测评&#xff0c;构筑了一座坚实的数字安全桥梁&#xff0c;其过程和条件清楚而又重要。这篇文章会详细介绍一些必要的步骤&#xff0c;以帮助你顺利地完成信息系统的安全和合规。 1. 系统识别与自评 在此基础上&#xff0c;首先要明确信息系统所承载的业务…

Zabbix配置监控参考

1 添加host 配置-主机-创建主机 添加主机名&#xff0c;IP&#xff0c;端口 2 添加监控项 配置-主机-监控项 打开后&#xff0c;点击右上角添加监控项&#xff08;进去后。配置想要的监控项目&#xff09; 3 添加CPU监控项 需求&#xff1a;CPU使用率 实现&#xff1…

【基础篇】Docker 容器操作 FOUR

嘿&#xff0c;小伙伴们&#xff01;我是小竹笋&#xff0c;一名热爱创作的工程师。在上一篇文章中&#xff0c;我们探讨了 Docker 镜像管理的相关知识。今天&#xff0c;让我们一起深入了解一下 Docker 容器的操作吧&#xff01; &#x1f4e6; 运行、停止和删除容器 Docker…

归并排序 python C C++ 代码及解析

一&#xff0c;概念及其介绍 归并排序&#xff08;Merge sort&#xff09;是建立在归并操作上的一种有效、稳定的排序算法&#xff0c;该算法是采用分治法(Divide and Conquer&#xff09;的一个非常典型的应用。将已有序的子序列合并&#xff0c;得到完全有序的序列&#xff…

商家转账到零钱开通最快捷径

商家转账到零钱存在一定的捷径&#xff0c;这一捷径将放在文章最后。如果商家希望自行开通&#xff0c;可以按照以下步骤进行申请&#xff1a; 1. 确认主体资格&#xff1a;申请主体必须是公司性质&#xff08;有限公司类型&#xff09;&#xff0c;个体工商户暂不支持申请&…

企业级Linux系统防护

一、企业级Linux系统防护概述 一&#xff09;企业级Linux系统安全威胁 企业级Linux系统安全威胁列表 解决的主要安全威胁安全威胁牵涉到的人员及操作文件系统防护避免有意/无意的文件篡改、越权访问&#xff0c;根用户&#xff08;root&#xff09;权限泛滥企业内部用户误操作、…

【Golang 面试 - 基础题】每日 5 题(九)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

Linux虚拟化技术KVM

文章目录 虚拟化基础什么是虚拟化虚拟化优势虚拟机虚拟机的主要特征Hypervisor类型类型1&#xff1a;裸金属型类型2&#xff1a;宿主型 KVM概述KVM体系结构KVM模块载入后的系统运行模式KVM集中管理和控制宿主机环境准备 安装KVM工具包libvirt包功能libvirt结构图安装KVM相关包C…

SEO优化 prerender-spa-plugin工具使用 踩坑记录

安装prerender-spa-plugin yarn add prerender-spa-plugin 或 npm install prerender-spa-plugin初始配置 后面记录踩的坑 配置路由 const routes [{path: /,redirect: {path: /HomeView},},{path: /home,redirect: {path: /HomeView},},{ path: /HomeView,component: HomeV…

postgresql密码复杂度验证和有效期

前言 为了数据库安全以及应对等保测评等要求&#xff0c;我们需要设置密码复杂度。我们通过passwordcheck模块实现复杂度检测功能。 启用密码复杂度验证 找到自己安装pg库的配置文件目录&#xff0c;修改postgresql.conf vim postgresql.conf修改如下内容 shared_preload_…

2023版IDEA安装通义灵码屡遭挫败:重复尝试,安装依旧失败

目录 背景: 过程: 第一步: 第二步: 第三步: 安装成功: 总结: 通义灵码的优点: 背景: 小编使用的是2023版本IDEA&#xff0c;在安装通义灵码的时候出现了一件很让人头痛的问题&#xff0c;我在IEDA中的插件中心里面去下载&#xff0c;但是当我我安装的进度条加载完成之…

命令行创建git仓库

方法1&#xff1a;初始化自己的仓库 git init创建完成之后可以用ls -a查看是否存在.git文件 如果不想要git仓库&#xff0c;可以使用rm -rf .git删除仓库 方法2&#xff1a;克隆别人的仓库 git clone [http][http]是仓库网址 总体流程 可以看到文件分为四种状态&#xff0c…

windows无法打开添加打印机原因分析及解决方法

在日常办公和生活中&#xff0c;打印机是不可或缺的重要设备。然而&#xff0c;有时在添加打印机的过程中&#xff0c;经常会遇各种问题。今天有个小伙伴问我windows无法打开添加打印机怎么回事&#xff1f;今天就教大家windows无法打开添加打印机原因分析及解决方法。 添加打打…

氧传感器在码头油气回收船岸安全装置中的重要作用

随着全球对环境保护和安全生产要求的日益提升&#xff0c;石化码头的油气回收问题已成为行业关注的焦点。在汽油、航煤、苯、对二甲苯等油品和化学品的装船过程中&#xff0c;大量油气挥发不仅加剧了大气污染&#xff0c;还潜藏着对人体健康的严重威胁。因此&#xff0c;推广和…

芋道以开源之名行下作之事 恬不知耻 标榜自己开源 公开源码+sql 不用再加入知识星球

资源 链接: https://pan.baidu.com/s/1TeuxbAUfLQ5_BqMBF1kniQ?pwdcqud 提 取码: cqud 依次为后端、补充版的sql、前端 此文档内安装部署等一应俱全

天气预报的爬虫内容打印并存储用户操作

系统名称&#xff1a; 基于网络爬虫技术的天气数据查询系统文档作者&#xff1a;清馨创作时间&#xff1a;2024-7-29最新修改时间&#xff1a;2024-7-29最新版本号&#xff1a; 1.0 1.背景描述 该系统将基于目前比较流行的网络爬虫技术&#xff0c;对网站上&#xff08;NowAPI…

数据结构之八大排序(上)

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a;数据结构&#xff08;Java版&#xff09; 目录 排序的相关介绍 直接插入排序 希尔排序&#xff08;缩小增量排序&#xff09; 选择排序 …