Linux 进程间通信与线程同步技术详解:IPC 机制、线程 API、同步工具与经典同步问题

进程间通信(IPC)

  • Inter_Process Communication

通信方式 双工模式 特点
管道(无名 / 有名) 半双工 同一时间只能单向
共享内存 全双工 双向同时读写
消息队列 全双工 双向收发消息
Socket 全双工 网络双向通信

管道

  • 管道文件 内存分配空间,速度快 同步,阻塞 (创建----->mkfifo (first in first out)) 需要读写同时打开

    • 有名管道---->任意进程

      cpp 复制代码
      //写入
      #include<stdio.h>
      #include<string.h>
      #include<unistd.h>
      #include<stdlib.h>
      #include<fcntl.h>
      #include<signal.h>
      void fun(int sig)
      {
          printf("sig=%d",sig);
      }
      int main()
      {
            signal(SIGPIPE,fun);
            int fd=open("fifo",O_WRONLY);
            if(fd==-1)
            {
                printf("open fifo err!!!\n");
                exit(1);
            }
            printf("open fifo success\n");
            while(1)
            {
                char buff[128]={0};
                fgets(buff,128,stdin);//会吸收\n
                if(strncmp(buff,"end",3)==0)
                {
                    break;
                }
                write(fd,buff,strlen(buff)-1); 
            }
            close(fd);
            exit(0);
      }    
      //读端
      int main()
      {
          int fd=open("fifo",O_RDONLY);
        if(fd==-1)
          {
              printf("open err!!!\n");
          }
          while(1)
          {
              char buff[128]={0};
              int n=read(fd,buff,127);
              if(n==0)
              {
                  break;
              }
              printf("buff=%s",buff);
          }
          close(fd);
      }
    • 无名管道----> 父子 pipe(int fd[2])

      cpp 复制代码
      #include<stdio.h>
      #include<string.h>
      #include<unistd.h>
      #include<stdlib.h>
      #include<fcntl.h>
      #include<signal.h>
      int main()
      {
          int fd[2];
          int res=pipe(fd);//fd[0] 读端  fd[1] 写端
          if(res==-1)
          {
              printf("pipe err!!!\n");
              exit(1);
          }
          pid_t pid=fork();
          if(pid==-1)
          {
              exit(1);
          }
          if(pid==0)
          {
              close(fd[1]);
              while(1)
              {
                  char buff[128]=0;
                  int n=read(fd[0],buff,127);
                  if(read==0)
                  {
                      break;
                  }
                  printf("buff=%s\n",buff);
              }
            close(fd[0]);
          }
        else
          {
              close(fd[0]);
              while(1)
              {
                  char buff[128]={0};
                  printf("input:");
                  fgets(buff,128,stdin);
                  if(strncmp(buff,"end",3)==0)
                  {
                      break;
                  }
                  write(fd[1],buff,strlen(buff)-1);
              }
              close(1);
          }
          exit(0);
      }

面试

1.有名管道和无名管道区别?

  • 有名管道 可以在任意两个进程间通信

  • 无名管道 只可以在父子/兄弟进程间通信

特性 无名管道(Pipe) 有名管道(FIFO)
创建方式 pipe() 系统调用 mkfifo() 系统调用 / mkfifo 命令
标识 / 访问方式 无文件名,仅通过文件描述符 有文件名(存在于文件系统),可通过路径访问
通信进程关系 仅限有亲缘关系的进程(父子 / 兄弟) 任意进程(无亲缘关系也可)
生命周期 随进程退出而销毁 随文件系统存在(需手动 rm 删除)
存在形式 内存中,不可见 文件系统中有标识(ls -l 显示 p 类型),但数据仍在内存
打开方式 只能单向(半双工),需两个 pipe 实现双向 可单向 / 双向,通过 open() 打开文件即可

2.管道通信方式? 单工,半双工,全双工

  • 半双工 (同一时间只能一个方向)

管道类型 单向通信 双向通信
无名管道 关一端,用一个管道 必须造两个管道,各负责一个方向
有名管道 只开读 / 只开写,用一个管道 一个管道 + O_RDWR 打开,或造两个管道

3.写管道的数据在哪里?

  • 内存

信号量:

  • 多进程 / 多线程间资源竞争同步

信号和信号量的区别:

  • 信号 (signal)---->软中断 ,用于通知进程发生了某种事件

    • 作用:进程可以通过注册信号处理函数来响应不同的信号
  • 信号量 (semaphore)---->同步 工具,用于管理对共享资源的访问,通常为一个整数计数器 ,用来控制共享资源并发访问

    • 多线程/多进程 间资源的同步竞争
  • 总结

    • 信号 ----->进程间/进程内 事件的通知和处理 ---->异步通信机制

    • 信号量 ---->进程/线程间同步和互斥 ,控制对共享资源并发访问

信号量的使用

一、核心函数解析

1. semget ------ 创建 / 获取信号量集
cpp 复制代码
int semget(key_t key, int num_sems, int sem_flags);
  • 功能 :根据 key 创建一个新的信号量集,或获取一个已存在的信号量集的 ID。

  • 参数

    • key:类似文件名,用于标识 信号量集,多个进程可通过相同的 key 访问同一信号量集。

    • num_sems:信号量集中包含的信号量个数

    • sem_flags:权限掩码和创建标志,如 IPC_CREAT(不存在则创建)、IPC_EXCL(与IPC_CREAT联用,存在则失败)、0666(权限)。

  • 返回值 :成功返回信号量集 ID(sem_id),失败返回 - 1。

2. semctl ------ 控制信号量集
cpp 复制代码
int semctl(int sem_id, int sem_num, int command, ...);
  • 功能 :对信号量集 或其中的单个信号量 执行各种控制操作,如设置值获取值删除等。

  • 参数

    • sem_id:由 semget 返回的信号量集 ID

    • sem_num信号量 在集中的索引 (从 0 开始),某些命令(如IPC_RMID)会忽略此参数。

    • command:要执行的控制命令,常见的有:

      • SETVAL:设置指定信号量的值。

      • GETVAL:获取指定信号量的值。

      • IPC_RMID:立即删除信号量集,唤醒所有等待的进程。

    • ...可变参数 ,根据command的不同,可能需要传入一个union semun结构体,用于设置或返回值。

  • 返回值 :成功时,返回值取决于command;失败返回 - 1。

3. semop ------ 对信号量集执行原子操作
cpp 复制代码
int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
  • 功能 :对信号量集中的一个或多个信号量执行 P/V 操作 ,所有操作作为一个原子整体完成,要么全部成功,要么全部不执行。

  • 参数

    • sem_id:信号量集 ID。

    • sem_ops:指向一个 struct sembuff数组的指针,每个元素描述一个操作。

    cpp 复制代码
    struct sembuf {
        unsigned short sem_num;  // 信号量索引
        short          sem_op;   // 操作值:正数为V操作,负数为P操作,0为等待信号量变为0
        short          sem_flg;  // 操作标志,如IPC_NOWAIT(非阻塞)、SEM_UNDO(进程退出时撤销操作)
    };
    • num_sem_opssem_ops一次性执行 多少个 信号量操作,和p,v统一 。
  • 返回值:成功返回 0,失败返回 - 1。


二、关键要点

  1. 头文件依赖sys/sem.h``sys/types.h``sys/ipc.h

  2. 与 POSIX 信号量的区别

    特性 System V 信号量 POSIX 信号量
    操作对象 信号量集(多个信号量) 单个信号量
    原子性 支持对多个信号量的原子操作 仅支持单个信号量的原子操作
    持久性 内核持久,进程退出后仍存在,需显式删除 无名信号量随进程销毁;有名信号量需显式删除
    接口复杂度 较复杂,函数参数多 较简单,易于使用
  3. 典型使用流程

    1. semget 创建或获取信号量集。

    2. semctl 初始化 信号量的

    3. semop 执行 P/V 操作来同步进程

    4. semctlIPC_RMID命令)删除不再使用的信号量集。

三、实现

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<time.h>
#include"sem.h"
//a.c
int main()
{
    int n;
    sem_init();
    srand(time(0));
    for(int i=0;i<5;i++)
    {
        sem_p();//获取资源 -1
        printf("A");
        fllush(stdout);
        n=rand()%3;
        sleep(n);
        
        
        printf("A");
        fflush(stdout);
        
        sem_v();//释放资源 +1
        n=rand()%3;
        sleep(n);
    }
    sem_destroy();
    return 0;
}
//b.c
int main()
{
    int n;
    sem_init();
    srand(time(0));
    for(int i=0;i<5;i++)
    {
        sem_p();
        printf("B");
        fflush(stdout);
        n=rand()%3;
        sleep(n);
        
        
        printf("B");
        fllush(stdout);
        
        sem_v();
        n=rand()%3;
        sleep(n);
    }
    return 0;
}

./a& ./b&   -------->同时运行(后台)
cpp 复制代码
//sem.h
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/sem.h>

//初始化信号量值时,自己创建设置
union semun
{
    int val;
};

//创建信号量,初始;获取已存在信号量
void sem_init();

void sem_p();

void sem_v();

void sem_destroy();
cpp 复制代码
#include"sem.h"
static int semid=-1;

//创建信号量,初始;获取已存在信号量
void sem_init()
{
    //IPC_EXCL---->(execlusive 排他的、互斥的、独有的)
    semid=semget((key_t)1234,1,IPC_CREAT|IPC_EXCL|0600);//创建一个全新的
    if(semid==-1)//全新创建失败,或已创建--->获取
    {
        semid=semget((key_t)1234,1,0600);
        if(semid==-1)
        {
            printf("sem create err!!!\n");
        }
    }
    else
    {
		//初始化信号量
        union semun a;
        a.val=1;
        if(semctl(semid,0,SETVAL,a)==-1)//初始化信号量
        {
            printf("semctl setval err!!!\n");
        }
    }
}

void sem_p()
{
    struct sembuf buf;
    buf.sem_num=0;
    buf.sem_op=-1;
    buf.sem_flg=SEM_UNDO;
    if(semop(semid,&buf,1)==-1)
    {
        printf("p err!!!\n");
    }
}

void sem_v();
{
     struct sembuf buf;
    buf.sem_num=0;
    buf.sem_op=1;
    buf.sem_flg=SEM_UNDO;
    if(semop(semid,&buf,1)==-1)
    {
        printf("v err!!!\n");
    }
}

void sem_destory();
{
    if(semctl(semid,0,IPC_RMID)==-1)
    {
        printf("semctl err\n");
    }
}

key_t key与semid的区别

key_t key ------->find IPC资源 全局地址,所有进程一样

semid------------->operate 资源的

  • 特点:

    • 只在当前进程 内有效,不能传给别的进程

    • 不同进程操作同一个信号量 ,用的 semid 通常不同。

共享内存(Shared Memory)

共享内存 是 最快 的进程间通信(IPC)方式 ,它允许多个进程直接访问同一块 物理内存区域,数据不需要在进程间复制传输,是性能最高的 IPC 机制。

核心原理

  1. 操作系统开辟一块公共物理内存

  2. 多个进程将这块内存映射到自己的虚拟地址空间

  3. 进程直接读写这块内存,完成数据交换

  4. 无需内核中转,无数据拷贝,速度极快


关键特点

优点

  • 速度最快 :无内核拷贝,直接内存访问

  • 效率极高:适合大量数据、高频通信场景

  • 支持多进程:任意数量进程共享同一块内存

缺点

  • 无同步机制:进程同时读写会产生数据混乱(必须配合信号量 / 互斥锁)

  • 数据不安全:需要开发者手动处理并发问题


ipcs = IPC status

作用:查看系统里的 进程间通信 (IPC) 资源

cpp 复制代码
ipcs -s    //查看信号量
ipcs -m   //共享内存
ipcs -q   //消息队列

    
    
ipcrm -m shid//删除共享内存

Linux 下共享内存 API(C 语言)

Linux 提供 sys/shm.h 标准接口,核心四步:

shmget = shared memory get(获取 / 创建共享内存)

shmdt = shared memory detach(卸载 / 分离)---->断开连接

shmctl = shared memory control(控制 / 删除)

1. 创建 / 获取共享内存

  • 向操作系统申请 / 找到 那块公共内存
cpp 复制代码
int shmget(key_t key, size_t size, int shmflg);
  • key:唯一标识(多个进程用同一个 key 访问同一块内存)

  • size:内存大小(字节)

  • shmflg:权限 + 创建模式(IPC_CREAT 不存在则创建)

2. 挂载到进程地址空间

  • 映射自己的内存地址空间------->进程才可以访问
cpp 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 返回值:进程可用的虚拟内存指针

  • shmaddr=NULL:系统自动分配地址(推荐)

为什么要挂载?

因为:

  • 共享内存是系统公共区域

  • 进程不能直接访问

  • 必须挂载(映射) 到进程自己的空间

  • 才能像普通变量一样读写

3. 读写数据

直接像操作普通内存一样读写指针即可。

4. 卸载 + 删除共享内存

cpp 复制代码
// 卸载    断开连接   
int shmdt(const void *shmaddr);

// 删除(所有进程卸载后执行才会真正释放)    删除内存
int shmctl(int shmid, IPC_RMID, NULL);

完整示例(两个进程通信)

写进程(写入数据)

读进程(读取数据)

cpp 复制代码
#include "shm.h"
int main()
{
    int shmid=shmget((key_t)1234,SIZE,IPC_CREAT|0600);
    if(shmid==-1)
    {
        exit(1);
    }
    char* s=(char*)shmat(shmid,NULL,0);//建立映射关系
    if(s==(char*)-1)
    {
        exit(1);
    }



    while(1)
    {
        char buff[128]={0};
        fgets(buff,128,stdin);
        if(strncmp(buff,"end",3)==0)
        {
            break;
        }
        strcpy(s,buff);
        
    }
    shmdt(s);//断开映射关系
    exit(0);
}

cpp 复制代码
#include "shm.h"
int main()
{
    int shmid=shmget((key_t)1234,SIZE,IPC_CREAT|0600);
    if(shmid==-1)
    {
        exit(1);
    }
    char* s=(char*)shmat(shmid,NULL,0);//建立映射关系
    if(s==(char*)-1)
    {
        exit(1);
    }



    while(1)
    {
        if(s==NULL)
        {
            break;
        }
        printf("read=%s",s);
        sleep(1);
        
    }
    shmdt(s);//断开映射关系
    shmctl(shmid,IPC_RMID,NULL);//删除共享内存
    exit(0);
}

必须注意:同步问题

共享内存自带并发风险,两个进程同时写会导致数据错乱。

解决方案

  • 信号量(Semaphore)

  • 互斥锁(Mutex)

  • 管道 / 消息队列做同步通知


共享内存+2个信号量

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#define SIZE 1024

//信号量声明
#include<sys/sem.h>
int semid;
enum INDEX{SEM1,SEM2};
#define SEM_SIZE 2
union semun
{
    int val;
};
void sem_init();
void sem_p(enum INDEX in);
void sem_v(enum INDEX in);
void sem_destory();

//信号量定义
void sem_init()
{
    int semid=semget((key_t)1234,SEM_SIZE,IPC_CREAT|IPC_EXCL|0600);
    if(semid==-1)
    {
        semid=semget((key_t)1234,SEM_SIZE,0600);//获取已存在的信号量
        if(semid==-1)
        {
            printf("semget err!!!\n");
            exit(1);
        }
    }
    else
    {
        union semun a;
        int val[SEM_SIZE]={1,0};
        for(int i=0;i<SEM_SIZE;i++)
        {
            a.val=val[i];
            if(semctl(semid,i,SETVAL,a)==-1)
            {
                printf("sem init err!!!\n");
                exit(1);
            }
        }
    }
}
void sem_p(enum INDEX in)
{
    struct sembuf buf;
    buf.sem_num=in;
    buf.sem_op=-1;
    buf.sem_flg=SEM_UNDO;
    if(semop(semid,&buf,1)==-1)
    {
        printf("p err!!!\n");
    }
}
void sem_v(enum INDEX in)
{
    struct sembuf buf;
    buf.sem_num=in;
    buf.sem_op=1;
    buf.sem_flg=SEM_UNDO;
    if(semop(semid,&buf,1)==-1)
    {
        printf("v err!!!\n");
    }
}
void sem_destory()
{

    if(semctl(semid,0,IPC_RMID)==-1)
    {
        printf("sem destroy err!!!\n");
    }
}


//写端
int main()
{
    int shmid=shmget((key_t)1234,SIZE,IPC_CREAT|0600);
    sem_init();
    if(shmid==-1)
    {
        exit(1);
    }
    char* s=(char*)shmat(shmid,NULL,0);//建立映射关系
    if(s==(char*)-1)
    {
        exit(1);
    }



    while(1)
    {
        char buff[128]={0};
        fgets(buff,128,stdin);
        sem_p(SEM1);
        strcpy(s,buff);
        sem_v(SEM2);

        
        if(strncmp(buff,"end",3)==0)
        {
            break;
        }
    }
    shmdt(s);//断开映射关系
    exit(0);
}



//读端
#include"sem.h"
int main()
{
    int shmid=shmget((key_t)1234,SIZE,IPC_CREAT|0600);
    sem_init();
    if(shmid==-1)
    {
        exit(1);
    }
    char* s=(char*)shmat(shmid,NULL,0);//建立映射关系
    if(s==(char*)-1)
    {
        exit(1);
    }

    while(1)
    {
        sem_p(SEM2);
        if(strncmp(s,"end",3)==0)
        {
            break;
        }
        printf("read=%s",s);
        sem_v(SEM1);
        
    }
    shmdt(s);//断开映射关系
    shmctl(shmid,IPC_RMID,NULL);//删除共享内存
    sem_destroy();//销毁信号量
    exit(0);
}

常用命令(Linux)

cpp 复制代码
# 查看系统共享内存
ipcs -m

# 删除指定共享内存
ipcrm -m [shmid]

总结

  1. 共享内存 = 最快 IPC:无拷贝、直接访问物理内存

  2. 核心四步:创建 → 挂载 → 读写 → 卸载 / 删除

  3. 致命缺陷 :无同步,必须配合锁 / 信号量使用

  4. 适用场景:高频、大数据量的进程通信

共享内存和管道的对比

特点 管道 共享内存
速度 慢(拷贝 2 次)(内核缓冲区) 最快(0 拷贝)(内存)
同步 自带同步 无同步,必须用信号量
数据方式 流式 内存块
复杂度 简单 复杂
方向 半双工 全双工
适用场景 简单、少量数据 高频、大数据量

消息队列

Linux 消息队列是内核维护的带类型的消息链表,是进程间异步通信的经典 IPC 机制,支持按类型选择性接收、消息持久化,适合解耦、异步任务与多进程协作。


一、核心概念与特性

  • 本质:内核维护的消息链表,每条消息含 类型(mtype,>0) 与数据,支持按类型过滤读取。

  • 优势

    • 异步通信:发送方写入后即可继续,接收方可稍后读取。

    • 按类型接收 :可只取指定类型消息,实现定向通信

    • 持久化:队列与消息独立于进程,进程退出后仍保留。

    • 双向 / 多对多:多进程可读写同一队列。

  • 两种标准

    • System V 消息队列 :传统、兼容性好,接口为 msgget/msgsnd/msgrcv/msgctl

    • POSIX 消息队列 :接口更现代(mq_open/mq_send),支持优先级、超时,部分系统需额外库支持。

二、System V 消息队列(最常用)

1. 消息结构体(必须自定义)
cpp 复制代码
// 约定:第一个字段必须是 long 类型的消息类型
struct msgbuf {
    long mtype;       // 消息类型(>0)
    char mtext[1024]; // 消息数据(可自定义为任意结构体)
};
2. 核心系统调用(头文件:``)
(1) 创建 / 获取队列:msgget
cpp 复制代码
int msgget(key_t key, int msgflg);
  • key :队列唯一标识(可用 ftok 生成,或直接用整数如 0x1234)。

  • msgflg

    • IPC_CREAT:不存在则创建,存在则返回 ID。

    • IPC_EXCL:与 IPC_CREAT 联用,存在则报错(确保新建)。

    • 权限位:如 0666(所有用户可读写)。

  • 返回 :成功返回 msgid(队列 ID),失败返回 -1

(2) 发送消息:msgsnd(message send)
cpp 复制代码
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msgp:指向消息结构体指针。

  • msgsz数据部分长度 (不含 mtype,如 sizeof(msgbuf)-sizeof(long))。

  • msgflg0 表示队列满时阻塞;IPC_NOWAIT 表示非阻塞(满则返回 -1)。

  • 返回 :成功 0,失败 -1

(3) 接收消息:msgrcv(message recieve)
cpp 复制代码
size_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • msgtyp

    • 0:读取队列中第一条消息。

    • >0:只读取类型等于 msgtyp 的第一条消息。

    • <0:读取类型 ≤ |msgtyp|最小类型的消息。

  • msgflg0 阻塞;IPC_NOWAIT 非阻塞;MSG_NOERROR:数据超长时截断不报错。

  • 返回 :成功返回接收数据长度,失败 -1

(4) 控制 / 删除队列:msgctl
cpp 复制代码
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • cmd

    • IPC_STAT:获取队列信息到 buf

    • IPC_SET:设置队列属性(权限、大小等)。

    • IPC_RMID立即删除队列,并释放所有消息。

  • 返回 :成功 0,失败 -1

3. 完整示例(发送 / 接收)
发送端(msg_send.c)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>

#define MSG_KEY 0x1234
#define MSG_SIZE 128

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

int main() {
    // 1. 创建/获取队列
    int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
    if (msqid == -1) { perror("msgget"); exit(1); }

    // 2. 准备消息
    struct msgbuf msg;
    msg.mtype = 1; // 类型1
    strcpy(msg.mtext, "Hello from msg_send!");

    // 3. 发送消息
    if (msgsnd(msqid, &msg, MSG_SIZE, 0) == -1) {
        perror("msgsnd"); exit(1);
    }
    printf("消息发送成功\n");

    return 0;
}
接收端(msg_recv.c)
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>

#define MSG_KEY 0x1234
#define MSG_SIZE 128

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

int main() {
    // 1. 获取队列
    int msqid = msgget(MSG_KEY, 0666);
    if (msqid == -1) { perror("msgget"); exit(1); }

    // 2. 接收类型1的消息
    struct msgbuf msg;
    if (msgrcv(msqid, &msg, MSG_SIZE, 1, 0) == -1) {
        perror("msgrcv"); exit(1);
    }
    printf("收到类型%ld的消息:%s\n", msg.mtype, msg.mtext);

    // 3. 删除队列(可选,通常由最后一个进程清理)
    msgctl(msqid, IPC_RMID, NULL);
    return 0;
}
4. 编译与运行
cpp 复制代码
gcc msg_send.c -o msg_send
gcc msg_recv.c -o msg_recv
./msg_send  # 发送
./msg_recv  # 接收

三、常用命令(查看 / 删除队列)

  • 查看 系统消息队列:ipcs -q

  • 删除指定队列:ipcrm -qipcrm -Q

四、系统限制(可调整)

  • 单条消息最大长度:cat /proc/sys/kernel/msgmax(默认约 8KB)。

  • 队列总字节上限:cat /proc/sys/kernel/msgmnb

  • 系统最大队列数:cat /proc/sys/kernel/msgmni

  • 临时修改(root):sysctl -w kernel.msgmax=16384

  • 永久修改:编辑 /etc/sysctl.conf,添加 kernel.msgmax=16384,执行 sysctl -p

五、POSIX 消息队列(简要对比)

  • 接口:mq_open/mq_send/mq_receive/mq_close/mq_unlink

  • 优势:支持消息优先级、超时、文件系统路径命名、更规范的错误处理。

  • 劣势:部分嵌入式 / 旧系统支持不完善,需链接 -lrt

套接字(socket)

支持网络通信的全双工方式

见下章相关网络编程(TCP)

线程

进程 :一个正在运行程序

线程进程内部 的一条执行路径

内核级线程

cpp 复制代码
ps -eLf l greap +文件.exe     -L----->显示线程id

线程实现

cpp 复制代码
//mian.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include <pthread.h>
void *fun(void* arg)
{
    for(int i=0;i<5;i++)
    {
        printf("fun run\n");
        sleep(1);
    }
    pthread_exit("fun exit");
}
int main()
{
    pthread_t id;//定义线程id
    pthread_create(&id,NULL,fun,NULL);//创建后---->分配id
    
    for(int i=0;i<5;i++)
    {
        printf("main run\n");
        sleep(1);
    }
    char* s=NULL;
    pthread_join(id,(char**)&s);//等待线程结束
    exit(0);
}
//主线程  结束----->进程结束、退出
cpp 复制代码
void* fun(void*arg)
{
    int* p=(int*)arg;
    int idx=*p;
    printf("idx=%d\n",idx);
}
int main()    
{
    pthread_t id[5];
    int i=0;
    for(;i<5;i++)
    {
        pthread_create(&id[i],NULL,fun,&i);
    }
    for(i=0;i<5;i++)
    {
        pthread_join(id[i],NULL);
    }
    exit(0);
}
//随机打印---->线程启动快慢不一,i是共享的
//按顺序创建,但执行顺序不一,可能边创建边执行

//创建堆区
void* fun(void*arg)
{
    int* p=(int*)arg;
    int idx=*p;
    free(p);
    printf("idx=%d\n",idx);
}
int main()    
{
    pthread_t id[5];
    int i=0;
    for(;i<5;i++)
    {
        int* p=(int*)malloc(sizeof(int));
        *p=i;
        pthread_create(&id[i],NULL,fun,(void*)p);
    }
    for(i=0;i<5;i++)
    {
        pthread_join(id[i],NULL);
    }
    exit(0);
}






int g_val=1;
void* fun(void*arg)
{
   for(int i=0;i<1000;i++)
   {
       printf("g_val=%d\n",g_val++);
   }
}// g_val++ 非原子操作   
//1.先从内存上读取g_val 当前值
//2.g_val+1
//3.新值 写回 内存
int main()    
{
    pthread_t id[5];
    int i=0;
    for(;i<5;i++)
    {
        pthread_create(&id[i],NULL,fun,&i);
    }
    for(i=0;i<5;i++)
    {
        pthread_join(id[i],NULL);
    }
    exit(0);
}



//引入  信号量、互斥锁
int g_val=1;
//pthread_mutex_t mutex;
sem_t sem;
void* fun(void*arg)
{
   for(int i=0;i<1000;i++)
   {
       sem_wait(&sem);//p
       //pthread_mutex_lock(&mutex);//上锁
       printf("g_val=%d\n",g_val++);
       sem_pos(&sem);//v
       //pthread_mutex_unlock(&mutex);//解锁
   }
}
int main()    
{
    pthread_t id[5];
    sem_init(&sem,0,1);//0--->局部进程使用   初始化信号量=1
    //pthread_mutex_init(&mutex,NULL);
    int i=0;
    for(;i<5;i++)
    {
        pthread_create(&id[i],NULL,fun,&i);
    }
    for(i=0;i<5;i++)
    {
        pthread_join(id[i],NULL);
    }
    sem_destory(&sem);
    //pthread_mutex_destory(&mutex);
    exit(0);
}

编译

bash 复制代码
gcc -o main main.c -pthread

核心 API

API 作用
pthread_create 创建并启动线程
pthread_join 等待线程结束(阻塞)
pthread_exit 线程主动退出
pthread_detach 分离线程
函数 参数 作用
pthread_create (tid, NULL, 函数,参数) 创建线程
pthread_join (tid, & 返回值) 等线程、收资源
pthread_exit (返回值) 线程自己退出
pthread_self 拿自己线程 ID
pthread_cancel (tid) 杀死指定线程
pthread_detach (tid) 设为自动回收

并发与并行

线程并发运行

  • 并发 :1 个 CPU 核心,快速切换 多个线程(交替进行

  • 并行 :多个 CPU 核心,真正同时 运行多个线程(同时进行

操作系统角度分析,线程实现的三种

  • 用户级------>并发

  • 内核级------>并行

  • 组合模型

线程同步

目的多线程同时访问共享资源时,按照预定顺序进行,防止数据混乱、保证结果正确

  • 信号量(Semaphore)--->计数同步

  • 互斥锁(Mutex)----->最常用

  • 读写锁

  • 条件变量(Condition)------->配合使用

一、互斥锁 (最核心)

作用:同一时间只允许一个线程访问共享资源

1. 核心函数

cpp 复制代码
// 初始化锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

// 加锁(阻塞)
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

2. 参数说明

  • mutex:互斥锁变量

  • attr:属性,一般填 NULL(默认)

3. 使用规则

  • 进入共享区域前 lock

  • 退出共享区域后 unlock

  • 必须成对使用,否则会死锁


二、条件变量 pthread_cond_t(等待 + 唤醒)

作用:让线程等待某个条件成立,不浪费 CPU

常和 互斥锁

1. 核心函数

cpp 复制代码
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

// 等待条件成立(会自动释放锁,休眠后自动拿回锁)
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

// 唤醒一个等待线程
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);

// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

三、信号量 sem_t(计数同步)

作用:控制最多 N 个线程同时进入临界区

用于生产者 - 消费者

1. 头文件

cpp 复制代码
#include <semaphore.h>

2. 核心函数

cpp 复制代码
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);

// P操作:申请资源(-1),不足则阻塞
int sem_wait(sem_t *sem);

// V操作:释放资源(+1)
int sem_post(sem_t *sem);

// 销毁信号量
int sem_destroy(sem_t *sem);

3. 参数

  • pshared:0 = 线程间使用,非 0 = 进程间使用

  • value:初始资源数


四、自旋锁 pthread_spinlock_t

作用:不睡眠,循环等待,适合锁内操作极快的场景

核心函数

cpp 复制代码
pthread_spin_init();
pthread_spin_lock();
pthread_spin_unlock();
pthread_spin_destroy();

最经典:互斥锁完整示例代码(必背)

cpp 复制代码
#include <stdio.h>
#include <pthread.h>

int share = 0;        // 共享资源
pthread_mutex_t mutex; // 定义互斥锁

void* func(void* arg) {
    // 加锁
    pthread_mutex_lock(&mutex);

    share++;  // 安全访问共享资源
    printf("share = %d\n", share);

    // 解锁
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t t1, t2;

    // 初始化锁
    pthread_mutex_init(&mutex, NULL);

    pthread_create(&t1, NULL, func, NULL);
    pthread_create(&t2, NULL, func, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    // 销毁锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

编译:

bash 复制代码
gcc test.c -o test -pthread

线程同步 极简总结(面试 / 考试直接背)

  1. 互斥锁:同一时间只允许一个线程访问(最常用)

  2. 条件变量:线程等待条件,不占 CPU(配合锁)

  3. 信号量:控制 N 个线程同时进入(生产者消费者)

  4. 自旋锁:忙等待,锁内极快时使用

  5. 所有同步的目的:保证共享资源安全、不乱序、不冲突

线程同步 指的是当一个线程 在对某个临界资源进行操作 时,其他线程都不可以 对这个资 源进行操作,直到该线程完成操作, 其他线程才能操作,也就是协同步调 ,让线程按预定的 先后次序进行运行。

生产者和消费者问题

  • 有限缓冲区

  • 不可以同时访问 ----->互斥锁

  • 生产者、消费者 是否可以进行操作------>信号量

    • 缓冲区 -------->生产者等待

    • 缓冲区 --------->消费者等待

优点:

  • 解耦 :通过缓冲区,二者不会直接调用

  • 支持并发 :生产者和消费者---->独立的个体

  • 二者处理能力 达到动态平衡

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<pthread.h>
#include<semaphore.h>
#include<time.h>

#define BUFF_MAX 30

#define SC_NUM 2

#define XF_NUM 3

int in=0;
int out=0;
sem_t sem_empty;
sem_t sem_full;
phread_mutex_t mutex;

int buff[BUFF_MAX]={0};


void* sc_thread(void* arg)
{
 	int idx=(int)arg;
    while(1)
    {
        sem_wait(&sem_empty);//p操作
        pthread_mutex_lock(&mutex);//加锁
        buff[in]=rand()%100;
        printf("生产者%d 产生数据%d,in=%d",idx,buff[in],in);
        in=(in+1)%BUFF_MAX;
        
        pthread_mutex_unlock(&mutex);//解锁
        sem_post(&sem_full);//v操作--->消费者
        
        int n=rand()%10;
        sleep(n);
    }
}

void* xf_thread(void* arg)
{
    int idx=(int)arg;
    while(1)
    {
        sem_wait(&sem_full);//p操作
        phread_mutex_lock(&mutex);//加锁
        printf("消费者%d 消费数据%d,out=%d",idx,buff[out],out);
    }
    out=(out+1)%SUFF_MAX;
    phread_mutex_unlock(&mutex);//解锁
    sem_post(&empty_empty);//v操作--->生产者可以生产+1
    
    int n=rand()%10;
    sleep(n);  
}
int main()
{
    phread_mutex_init(&mutex,NULL);
    sem_init(&sem_empty,0,BUFF_MAX);//生产者  信号量  
    sem_init(&sem_full,0,0);//消费者 信号量
    srand((int)time(0));
    
    phread_t sc_id[SC_MAX];
    phread_t xf_id[XF_MAX];
    int i=0;
    for(;i<SC_MAX;i++)
    {
        phread_create(&sc_id[i],NULL,sc_thread,(void*)i);
    }
     for(i=0;i<xf_MAX;i++)
    {
        phread_create(&xf_id[i],NULL,xf_thread,(void*)i);
    }
     for(;i<SC_MAX;i++)
    {
        phread_join(&sc_id[i],NULL);
    }
    for(i=0;i<xf_MAX;i++)
    {
        phread_join(&xf_id[i],NULL);
    }
    
    sem_destory(&sem_empty);
    sem_destroy(&sem_full);
    pthread_mutex_destory(&mutex);
    exit(0);
}

线程安全=正确性

无论调度顺序如何,都可以得到正确的结果

多个线程 共享同一块内存 ,互相覆盖 、互相干扰数据竞争 → 结果错乱。

  • 同步:信号量、互斥锁、读写锁、条件变量

  • 使用线程安全的函数(可重入函数)

eg:strtock_r

strtok ----->内部使用**静态变量(static)**保存上次切割位置

  • 全局唯一线程共享不可重入

  • 多线程同时调用 互相干扰

strtok_r ----->自己传指针,线程独立

  • char *strtok_r(char *str, const char *delim, char **saveptr);
不安全 安全版本(线程可用) 作用
strtok strtok_r 切割字符串
asctime asctime_r 时间转字符串
ctime ctime_r 时间转字符串
gmtime gmtime_r 时间解析
localtime localtime_r 本地时间
gethostbyname gethostbyname_r DNS 解析

多线程fork()

子进程 只有一条执行路径(fork()所在的路径)

  • 千万不要在多线程程序里随便调用 fork ()

  • fork () 只会复制 当前调用线程, 其他线程 全部消失!

  • 父进程的锁、状态、全局变量 会被原样复制,极易死锁

问题 1:多线程中某个线程调用 fork (),子进程会有和父进程相同数量的线程吗?

结论

绝对不会!

子进程只有调用 fork () 的这一个线程其他所有线程在子进程中全部消失

底层原理

fork() 的设计规则:

  1. 只复制当前调用 fork 的线程的执行上下文

  2. 父进程的其他线程在子进程地址空间中直接终止,不会进入子进程

  3. 子进程 启动时,只有一个线程在运行

简单比喻

父进程是一个有 5 个人的办公室,只有 1 个人按下了复制按钮 ,复制出来的新办公室里,只有这一个人,其他 4 个人都不存在。


问题 2:父进程被加锁的互斥锁,fork 后在子进程中是否已经加锁?

结论

是的!子进程中的互斥锁会保持 "已加锁" 状态,且永远无法解锁!

底层原理

  1. fork() 会完整复制父进程的内存状态(包括互斥锁的状态)

  2. 如果父进程中某个线程持有锁,fork 时,锁的 "已加锁" 标记会被原样复制到子进程

  3. 关键:持有锁的那个线程,在子进程里已经消失了

  4. 结果:子进程里的锁永远处于加锁状态,无法解锁 ,任何尝试加锁的操作都会死锁

  5. fork 做的事只有两件:

    1. 复制整个内存(包括 mutex 那块数据)

    2. 只把线程 A 复制过去当子进程唯一线程

致命风险

这是多线程 + fork 最危险的坑

  • 子进程拿到一把永远锁死的互斥锁

  • 子进程调用任何依赖这个锁的函数(如 mallocprintf、线程库函数)都会直接死锁


核心总结(必背)

  1. 多线程调用 fork,子进程只有单线程,父进程其他线程全部丢失

  2. 父进程加锁的互斥锁,fork 后子进程中依然是加锁状态,且无法解锁,极易死锁

  3. 最佳实践 :多线程程序中,fork 后子进程必须立即调用 exec 系列函数替换进程,不要在子进程里做复杂操作

总结

  1. 多线程调用fork,子进程仅保留调用 fork 的单个线程,父进程其他线程全部消失

  2. 父进程已加锁的互斥锁,fork后子进程中保持加锁状态且永久无法解锁,会直接引发死锁

  3. 多线程环境下,fork后子进程应立刻调用 exec 函数,避免操作共享资源

读写锁

  • 读可以共享,写必须独占

  • 读锁(共享锁):多个线程可以同时加读锁

  • 写锁(独占锁):只能一个线程加写锁

规则:

  • 读 + 读 → 不互斥,可以并发

  • 读 + 写 → 互斥

  • 写 + 写 → 互斥

优点:读多写少场景,并发性能大幅提升

缺点:逻辑更复杂,可能出现写饥饿

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// 定义读写锁
pthread_rwlock_t rwlock;
int data = 0;

// 读线程
void* read_fun(void* arg)
{
    // 加读锁
    pthread_rwlock_rdlock(&rwlock);

    printf("线程%d 读 data=%d\n", (int)arg, data);
    sleep(1);  // 模拟读耗时

    // 解读锁
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

// 写线程
void* write_fun(void* arg)
{
    // 加写锁
    pthread_rwlock_wrlock(&rwlock);

    data++;
    printf("线程%d 写 data=%d\n", (int)arg, data);
    sleep(1);

    // 解写锁
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

int main()
{
    pthread_t tid1, tid2, tid3, tid4;

    // 初始化读写锁
    pthread_rwlock_init(&rwlock, NULL);

    // 创建读线程
    pthread_create(&tid1, NULL, read_fun, (void*)1);
    pthread_create(&tid2, NULL, read_fun, (void*)2);

    // 创建写线程
    pthread_create(&tid3, NULL, write_fun, (void*)3);
    pthread_create(&tid4, NULL, write_fun, (void*)4);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);
    pthread_join(tid4, NULL);

    // 销毁锁
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

读者和写者模型

提高并发效率,同时保证安全

读可以一起,写必须独占

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

// 共享数据
int data = 0;

// 读者计数器
int read_count = 0;

// 互斥锁:保护 read_count
pthread_mutex_t mutex;

// 读写互斥锁:读和写互斥,写和写互斥
sem_t rw_sem;//初始化为1

// 读者线程
void *reader(void *arg) {
    int id = *(int *)arg;

    // 进入临界区:修改 read_count
    pthread_mutex_lock(&mutex);
    read_count++;
    // 第一个读者,加写锁
    if (read_count == 1)
        sem_wait(&rw_sem);
    pthread_mutex_unlock(&mutex);

    // 读操作
    printf("读者 %d 读取 data = %d\n", id, data);

    // 离开临界区
    pthread_mutex_lock(&mutex);
    read_count--;
    // 最后一个读者,释放写锁
    if (read_count == 0)
        sem_post(&rw_sem);
    pthread_mutex_unlock(&mutex);

    return NULL;
}

// 写者线程
void *writer(void *arg) {
    int id = *(int *)arg;

    // 加写锁(独占)
    sem_wait(&rw_sem);

    // 写操作
    data++;
    printf("写者 %d 修改 data = %d\n", id, data);

    // 释放写锁
    sem_post(&rw_sem);

    return NULL;
}

int main() {
    pthread_t r1, r2, w1, w2;
    int id1 = 1, id2 = 2;

    // 初始化
    pthread_mutex_init(&mutex, NULL);
    sem_init(&rw_sem, 0, 1);

    // 创建线程
    pthread_create(&r1, NULL, reader, &id1);
    pthread_create(&r2, NULL, reader, &id2);
    pthread_create(&w1, NULL, writer, &id1);
    pthread_create(&w2, NULL, writer, &id2);

    // 等待结束
    pthread_join(r1, NULL);
    pthread_join(r2, NULL);
    pthread_join(w1, NULL);
    pthread_join(w2, NULL);

    // 销毁
    pthread_mutex_destroy(&mutex);
    sem_destroy(&rw_sem);

    return 0;
}

条件变量

加锁 = 强制让 "判断条件 + 进入等待" 变成一个不可分割的整体!

wait和signal 加锁----->保证操作的原子性

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t mutex;
pthread_cond_t cond;

char buff[128] = {0};

void * funa(void* arg)
{
    while( 1 )
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);//放在等待队列,解锁,自己阻塞。唤醒返回时加锁
        pthread_mutex_unlock(&mutex);

        if( strncmp(buff,"end",3) == 0 )
        {
            break;
        }
        printf("funa buff=%s\n",buff);
    }
}
void * funb(void* arg)
{
    while( 1 )
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);//放在等待队列,解锁,自己阻塞。唤醒返回时加锁
        pthread_mutex_unlock(&mutex);
        if( strncmp(buff,"end",3) == 0 )
        {
            break;
        }
        printf("funb buff=%s\n",buff);
    }

}

int main()
{
    pthread_mutex_init(&mutex,NULL);//锁
    pthread_cond_init(&cond,NULL);//条件变量初始化

    pthread_t ida, idb;
    pthread_create(&ida,NULL,funa,NULL);
    pthread_create(&idb,NULL,funb,NULL);

    while( 1 )
    {
        fgets(buff,128,stdin);

        if( strncmp(buff,"end",3) == 0 )
        {
            //唤醒所有
            pthread_mutex_lock(&mutex);
            pthread_cond_broadcast(&cond);
            pthread_mutex_unlock(&mutex);
            break;
        }
        else
        {
            //唤醒一个线程
            pthread_mutex_lock(&mutex);
            pthread_cond_signal(&cond);
            pthread_mutex_unlock(&mutex);
        }
    }

    pthread_join(ida,NULL);
    pthread_join(idb,NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    exit(0);
}

条件是共享变量 ,多线程访问必须加锁

wait 加锁:保护条件,防止判断错乱

signal 加锁:防止丢信号,保证一定唤醒

✅ 同一把锁:让等待和唤醒有序,不会乱序

生产者和消费者(条件变量+互斥锁)

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define MAX 5   // 缓冲区最大容量

// 共享缓冲区
int buf[MAX];
int head = 0;   // 队头(取数据)
int tail = 0;   // 队尾(放数据)
int size = 0;   // 当前数据个数

pthread_mutex_t mutex;
pthread_cond_t not_full;  // 不满 -> 生产者可以放
pthread_cond_t not_empty; // 不空 -> 消费者可以取

// 生产者:生产数据
void *producer(void *arg)
{
    int num = 0;
    while (1) {
        pthread_mutex_lock(&mutex);

        // ======================
        // ✅ 关键:队列满了就等待!
        // ======================
        while (size == MAX) {
            printf("队列满了,生产者等待...\n");
            pthread_cond_wait(&not_full, &mutex);
        }

        // 生产数据
        buf[tail] = ++num;
        printf("生产者生产:%d\n", num);
        tail = (tail + 1) % MAX;
        size++;

        // 唤醒消费者
        pthread_cond_signal(&not_empty);

        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    return NULL;
}

// 消费者:消费数据
void *consumer(void *arg)
{
    while (1) {
        pthread_mutex_lock(&mutex);

        // ======================
        // ✅ 关键:队列空了就等待!
        // ======================
        while (size == 0) {
            printf("队列空了,消费者等待...\n");
            pthread_cond_wait(&not_empty, &mutex);
        }

        // 消费数据
        int val = buf[head];
        printf("消费者消费:%d\n", val);
        head = (head + 1) % MAX;
        size--;

        // 唤醒生产者
        pthread_cond_signal(&not_full);

        pthread_mutex_unlock(&mutex);
        sleep(2);
    }
    return NULL;
}

int main()
{
    pthread_t pro, con;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);

    pthread_create(&pro, NULL, producer, NULL);
    pthread_create(&con, NULL, consumer, NULL);

    pthread_join(pro, NULL);
    pthread_join(con, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);

    return 0;
}

pthread_cond_wait 醒来 后,必须重新检查条件

while----->防止被虚假唤醒

相关推荐
特长腿特长2 小时前
centos、ubantu系列机的用户和用户组的结构是什么?具体怎么配置?用户组权限怎么使用?这篇文章持续更新,帮助你复习linux的基础知识
linux·运维·centos
zzzyyy5382 小时前
Linux环境变量
linux·运维·服务器
pluvium273 小时前
记对 xonsh shell 的使用, 脚本编写, 迁移及调优
linux·python·shell·xonsh
无级程序员3 小时前
centos7 安装 llvm-toolset-7-clang出错的问题解决
linux·centos
CHHC18803 小时前
NetCore树莓派桌面应用程序
linux·运维·服务器
云栖梦泽5 小时前
Linux内核与驱动:9.Linux 驱动 API 封装
linux·c++
si莉亚6 小时前
ROS2安装EVO工具包
linux·开发语言·c++·开源
Tingjct6 小时前
Linux常用指令
linux·运维·服务器
广州灵眸科技有限公司6 小时前
为RK3588注入澎湃算力:RK1820 AI加速卡完整适配与评测指南
linux·网络·人工智能·物联网·算法