进程间通信

进程间通信

管道

匿名管道

  • 也叫无名管道,它是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。
  • 匿名管道没有文件实体
$ ps auxf | grep mysql
  • 上面命令行里的「|」竖线就是一个管道,它的功能是将前一个命令(ps auxf)的输出,作为后一个命令(grep mysql)的输入,从这功能描述,可以看出管道传输数据是单向的, 半双工的。

  • 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。先创建管道后fork。

  • 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用lseek() 来随机的访问数据。

#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。
    pipefd[0] 对应的是管道的读端
    pipefd[1] 对应的是管道的写端
返回值:
    成功 0
    失败 -1
管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞
  • 注意事项:

    • 读管道:
      管道中有数据,read返回实际读到的字节数。
      管道中无数据:
      写端被全部关闭,read返回0(相当于读到文件的末尾)
      写端没有完全关闭,read阻塞等待

    • 写管道:
      管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
      管道读端没有全部关闭:
      管道已满,write阻塞
      管道没有满,write将数据写入,并返回实际写入的字节数

有名管道(FIFO)

  • 也叫命名管道、FIFO文件, 克服了匿名管道只能用于亲缘关系的进程间通信的缺点。

  • 虽然有名管道有文件实体,但是FIFO中的内容存放在内存中。

$ mkfifo myfifo
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:
    - pathname: 管道名称的路径
    - mode: 文件的权限 和 open 的 mode 是一样的,是一个八进制的数
返回值:成功返回0,失败返回-1,并设置错误号
  • 一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件I/O 函数都可用于 fifo。如:close、read、write、unlink 等。

  • 同样不支持诸如 lseek() 等文件定位操作。

消息队列

  • 消息队列是保存在内核中的消息链表,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。
  • 在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
  • 消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。
  • 缺点:一是通信不及时,二是消息也有大小限制
  • 消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAXMSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。
  • 消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

内存映射

  • 内存映射是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
  • 当两个进程映射同一个文件,我们就可以通过内存映射来实现进程间的通信。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
    - void *addr: NULL, 由内核指定
    - length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
            获取文件的长度:stat lseek
    - prot : 对申请的内存映射区的操作权限
        -PROT_EXEC :可执行的权限
        -PROT_READ :读权限
        -PROT_WRITE :写权限
        -PROT_NONE :没有权限
        要操作映射内存,必须要有读的权限。
        PROT_READ、PROT_READ|PROT_WRITE
    - flags :
        - MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
        - MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
    - fd: 需要映射的那个文件的文件描述符
        - 通过open得到,open的是一个磁盘文件
        - 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
            prot: PROT_READ                open:只读/读写 
            prot: PROT_READ | PROT_WRITE   open:读写
    - offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不偏移。
- 返回值:返回创建的内存的首地址
// 失败返回MAP_FAILED,(void *) -1

int munmap(void *addr, size_t length);
- 功能:释放内存映射
- 参数:
    - addr : 要释放的内存的首地址
    - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
  1. 有关系的进程(父子进程)

    • 还没有子进程的时候

    ​ - 通过唯一的父进程,先创建内存映射区

    • 有了内存映射区以后,创建子进程

    • 父子进程共享创建的内存映射区

  2. 没有关系的进程间通信

    • 准备一个大小不是0的磁盘文件
      - 进程1 通过磁盘文件创建内存映射区
    • 得到一个操作这块内存的指针
      - 进程2 通过磁盘文件创建内存映射区
    • 得到一个操作这块内存的指针
    • 使用内存映射区通信
  • 注意:内存映射区通信,是非阻塞的。

共享内存

  • 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
  • 与管道, 消息队列等要求发送进程将数据从用户空间的缓冲区复制进内核内存,和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
    新创建的内存段中的数据都会被初始化为0
- 参数:
    - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
            一般使用16进制表示,非0值
    - size: 共享内存的大小
    - shmflg: 属性
        - 访问权限
        - 附加属性:创建/判断共享内存是不是存在
            - 创建:IPC_CREAT
            - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
                IPC_CREAT | IPC_EXCL | 0664
    - 返回值:
        失败:-1 并设置错误号
        成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。


void *shmat(int shmid, const void *shmaddr, int shmflg);
- 功能:和当前的进程进行关联
- 参数:
    - shmid : 共享内存的标识(ID),由shmget返回值获取
    - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
    - shmflg : 对共享内存的操作
        - 读 : SHM_RDONLY, 必须要有读权限
        - 读写: 0
- 返回值:
    成功:返回共享内存的首(起始)地址。  失败(void *) -1


int shmdt(const void *shmaddr);
- 功能:解除当前进程和共享内存的关联
- 参数:
    shmaddr:共享内存的首地址
- 返回值:成功 0, 失败 -1

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进程被销毁了对共享内存没有影响。
- 参数:
    - shmid: 共享内存的ID
    - cmd : 要做的操作
        - IPC_STAT : 获取共享内存的当前的状态
        - IPC_SET : 设置共享内存的状态
        - IPC_RMID: 标记共享内存被销毁
    - buf:需要设置或者获取的共享内存的属性信息
        - IPC_STAT : buf存储数据
        - IPC_SET : buf中需要初始化数据,设置到内核中
        - IPC_RMID : 没有用,NULL

key_t ftok(const char *pathname, int proj_id);
- 功能:根据指定的路径名,和int值,生成一个共享内存的key
- 参数:
    - pathname:指定一个存在的路径
        /home/Linux/a.txt
    - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
               范围 : 0-255  一般指定一个字符 'a'
  • 操作系统如何知道一块共享内存被多少个进程关联?

    - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch

    - shm_nattach 记录了关联的进程个数

  • 可不可以对共享内存进行多次删除 shmctl

    • 可以的,因为shmctl 标记删除共享内存,不是直接删除

    - 什么时候真正删除呢?

    • 当和共享内存关联的进程数为0的时候,就真正被删除

​ - 当共享内存的key为0的时候,表示共享内存被标记删除了

  • 如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。

  • 共享内存和内存映射的区别

    1. 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)

    2. 共享内存效率更高

    3. 内存
      所有的进程操作的是同一块共享内存。
      内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。

    4. 数据安全

      • 进程突然退出
        共享内存还存在
        内存映射区消失
      • 运行进程的电脑死机,宕机了
        数据存在在共享内存中,没有了
        内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
    5. 生命周期
      - 内存映射区:进程退出,内存映射区销毁
      - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
      如果一个进程退出,会自动和共享内存进行取消关联。

信号

  • 在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:
$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

其中前 31 个信号为常规信号,其余为实时信号。

  • 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
    1. 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。
      • Ctrl+C 产生 SIGINT 信号,表示终止该进程;
      • Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;
    2. 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
    3. 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
    4. 运行 kill 命令或调用 kill 函数。
//发出信号
int kill(pid_t pid, int sig);
int raise(int sig); 
//定时器

unsigned int alarm(unsigned int seconds);

int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);
- 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
- 参数:
    - which : 定时器以什么时间计时
      ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM   常用
      ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM
      ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF
    - new_value: 设置定时器的属性

        struct itimerval {      // 定时器的结构体
            struct timeval it_interval;  // 每个阶段的时间,间隔时间
            struct timeval it_value;     // 延迟多长时间执行定时器
        };

        struct timeval {        // 时间的结构体
            time_t      tv_sec;     //  秒数     
            suseconds_t tv_usec;    //  微秒    
        };
    过10秒后,每个2秒定时一次
    - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
- 返回值:
    成功 0
    失败 -1 并设置错误号
  • 捕捉信号(SIGKILL SIGSTOP不能被捕捉,不能被忽略。)
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为
- 参数:
    - signum: 要捕捉的信号
    - handler: 捕捉到信号要如何处理
        - SIG_IGN : 忽略信号
        - SIG_DFL : 使用信号默认的行为
        - 回调函数 :  这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
    回调函数:
        - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
        - 不是程序员调用,而是当信号产生,由内核调用
        - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。

- 返回值:
    成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
    失败,返回SIG_ERR,设置错误号
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 功能:检查或者改变信号的处理。信号捕捉
- 参数:
    - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
    - act :捕捉到信号之后的处理动作
    - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
- 返回值:
    成功 0
    失败 -1

 struct sigaction {
    // 函数指针,指向的函数就是信号捕捉到之后的处理函数
    void     (*sa_handler)(int);
    // 不常用
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
    sigset_t   sa_mask;
    // 使用哪一个信号处理对捕捉到的信号进行处理
    // 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
    int        sa_flags;
    // 被废弃掉了
    void     (*sa_restorer)(void);
};

信号量

  • 信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据
//信号量的类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
    - 初始化信号量
    - 参数:
        - sem : 信号量变量的地址
        - pshared : 0 用在线程间 ,非0 用在进程间
        - value : 信号量中的值

int sem_destroy(sem_t *sem);
    - 释放资源

int sem_wait(sem_t *sem);
    - 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞

int sem_trywait(sem_t *sem);

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
    - 对信号量解锁,调用一次对信号量的值+1

int sem_getvalue(sem_t *sem, int *sval);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值