进程间通信(IPC)

目录 

一、管道

pipe函数

管道通信原理

实现pipe进程间通信

管道读写行为

练习:使用管道实现父子进程间 ls | wc -l 通信

练习:使用管道实现兄弟间通信

管道缓冲区大小

二、命名管道FIFO

创建方式

父子间通信

fifo实现非血缘关系进程间通信

三、文件

四、共享映射区(MMAP)

存储映射I/O

mmap函数 

mmap使用注意事项!!!

父子进程间通信

匿名映射mmap

无血缘关系进程间 mmap 通信

mmap总结

五、信号

信号机制

与信号相关的事件

产生信号

信号的处理方式

阻塞信号集&未决信号集

信号四要素

Linux 常规信号一览表

kill函数

父进程杀死指定子进程

raise函数

abort函数

alarm函数

测试一秒钟计数

setitimer 函数

使用setitimer定时,向屏幕打印信息

信号集操作函数!!!

sigprocmask 函数!!!

sigpending 函数

练习:把所有常规信号的未决状态打印至屏幕

信号捕捉

signal函数

sigaction 函数!!!

练习:sigaction捕捉SIGCHLD,回收子进程

补充细节:SIGCHLD信号

信号捕捉特性

中断系统调用

解决慢速系统调用中断

六、信号量

semget函数

semop函数

semctl函数

练习:进程间通信

七、消息队列

ftok函数

msgget函数

msgsnd函数

msgrcv函数

msgctl函数

练习:消息队列进程间通信

发送进程

接收进程

总结:


Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另 一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区, 进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

在进程间完成数据传递需要借助操作系统提供特殊的方法,如: 文件、管道、信号、共享内存、消息队列、套接字、命名管道 等。现今常用的进程间通信方式有:
<1> 管道 (使用最简单)、FIFO命名管道(无血缘关系)
<2> 信号 ( 开销最小 )
<3> 共享映射区 (mmap)  ( 无血缘关系 )
<4> 本地套接字 (socket) (
最稳定 )

一、管道

管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一 个管道。有如下特质:

<1> 其本质是一个伪文件(实为内核缓冲区)
<2> 由两个文件描述符引用,一个表示读端,一个表示写端。
<3> 规定数据从管道的写端流入管道,从读端流出。

管道的原理 : 管道实为内核使用环形队列机制,借助内核缓冲区 (4k) 实现。
管道的局限性:
<1> 数据不能进程自己写,自己读。
<2> 管道中数据不可反复读取。一旦读走,管道中不再存在。
<3>采用半双工通信方式,数据只能在单方向上流动。
<4> 只能在血缘关系进程间可用

pipe函数

#include <unistd.h>

创建,并打开管道。
int pipe(int pipefd[2]);
参数:	fd[0]: 读端。
	fd[1]: 写端。
返回值: 成功: 0
失败: -1 errno

管道通信原理

1. 父进程调用 pipe 函数创建管道,得到两个文件描述符 fd[0]、 fd[1]指向管道的读端和写端。
2. 父进程调用 fork 创建子进程,那么子进程也有两个文件描述符指向同一管道。
3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。
由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。

实现pipe进程间通信

#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>

int main(int argc,char*arv[]){

   int ret;
   int fd[2];
   char buf[1024];

   ret = pipe(fd);
   if(ret == -1){
        perror("pipe error");
        exit(1);
   }
   pid = fork();
   if(pid>0){
        close(fd[0]);
        write(fd[1],str,strlen(str));
        close(fd[1]);
   }else if(pid == 0){
        close(fd[1]);
        ret = read(fd[0],buf,sizeof(buf));
        write(STDOUT_FILENO,buf,ret);
        close(fd[0]);
   }else if(pid == -1){
       perror("fork error");
       exit(1);
   }
   return 0;
}

管道读写行为

读管道:
1. 管道有数据,read返回实际读到的字节数。
2. 管道无数据:
1)无写端,read返回0 (类似读到文件尾)
2)有写端,read阻塞等待。

写管道:
1. 无读端, 异常终止。 (SIGPIPE信号导致的)
2. 有读端:
1) 管道已满, 阻塞等待
2) 管道未满, 返回写出的字节个数。

练习:使用管道实现父子进程间 ls | wc -l 通信

假定父进程实现wc,子进程实现ls
ls命令正常会将结果集写到stdout,但现在会写入管道写端
wc -l命令正常应该从stdin读取数据,但此时会从管道的读端读。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>

int main(int argc,char*arv[]){

   int ret;
   int fd[2];
   char buf[1024];
   pid_t pid;

   ret = pipe(fd);
   if(ret == -1){
        perror("pipe error");
        exit(1);
   }
   pid = fork();
   if(pid>0){
        close(fd[0]);
        dup2(fd[1],STDOUT_FILENO);
        execlp("ls","ls",NULL);
   }else if(pid == 0){
        close(fd[1]);
        dup2(fd[0],STDIN_FILENO);
        execlp("wc","wc","-l",NULL);
   }else if(pid == -1){
       perror("fork error");
       exit(1);
   }
   return 0;
}

练习:使用管道实现兄弟间通信

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

int main(int argc,char*arv[]){

   int ret;
   int fd[2];

   ret = pipe(fd);
   if(ret == -1){
        perror("pipe error");
        exit(1);
   }

   pid_t pid,wpid;

   int i = 0;
   for(i = 0;i<2;i++){
       pid = fork();
       if(pid == 0){
           break;
       }
   }

   if(i == 0){
        close(fd[0]);
        dup2(fd[1],STDOUT_FILENO);
        execlp("ls","ls",NULL);
   }else if(i == 1){
        sleep(5);
        close(fd[1]);
        dup2(fd[0],STDIN_FILENO);
        execlp("wc","wc","-l",NULL);
   }else if(i == 2){
        close(fd[0]);
        close(fd[1]);
        while((wpid = waitpid(-1,NULL,WNOHANG))!=-1){
           if(wpid == 0){
                sleep(1);
                continue;
           }else if(wpid > 0){
                printf("child a finish:%d\n",getpid());
           }
       }
   }
   return 0;
}

注:父进程不使用管道,所以一定要关闭父进程的管道,保证数据单向流动
一个pipe可以有一个写端多个读端
一个pipe可以有多个写端一个读端

管道缓冲区大小

可以使用 ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。
等于4096

#include <unistd.h>

long fpathconf(int fd, int name);
成功: 返回管道的大小 失败: -1, 设置 errno

管道(pipe)
优点:简单,相比信号,套接字实现进程通信,简单很多
缺点:1.只能单向通信,双向通信需建立两个管道
           2.只能用于有血缘关系的进程间通信。

二、命名管道FIFO

FIFO 是 Linux 基础文件类型中的一种。但, FIFO 文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行 read/write, 实际上是在读写内核通道,这样就实现了进程间通信。

创建方式

<1> 命令:mkfifo 管道名
<2> 库函数:

int mkfifo(const char *pathname, mode_t mode); 
成功: 0; 失败: -1

一旦使用 mkfifo 创建了一个 FIFO, 就可以使用 open 打开它, 常见的文件 I/O 函数都可用于 fifo。 如: close、 read、write、 unlink 等。

父子间通信

#include <errno.h>
#include <string.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(int argc,char*arv[]){
   pid_t pid,wpid;

   char *say = "hello world!!!!\n";
   char buf[256];
   int ret = mkfifo("test_fifo",0644);
   if(ret == -1){
       perror("mkfifo error");
       exit(1);
   }

   pid = fork();

   if(pid == 0){
        int fd  = open("test_fifo",O_WRONLY);
        write(fd, say, strlen(say));
        close(fd);
        sleep(2);
   }else if(pid > 0){
        sleep(1);
        int rfd = open("test_fifo", O_RDONLY);
        int tmp = read(rfd, buf, sizeof(buf));
        write(STDOUT_FILENO,buf, tmp);
        close(rfd);
        wait(NULL);
   }
   return 0;
}

fifo实现非血缘关系进程间通信

fifo支持一个读端多个写段,也支持一个写端多个读端,但多个读端需注意数据一旦被读走就没了,所以多个读端的并集才是写端的写入数据

三、文件

文件实现进程间通信:
打开的文件是内核中的一块缓冲区。多个无血缘关系的进程,可以同时访问该文件。

文件通信这个,有没有血缘关系都行,
只是有血缘关系的进程对于同一个文件,使用的同一个文件描述符,
没有血缘关系的进程,对同一个文件使用的文件描述符可能不同。
这些都不是问题,打开的是同一个文件就行。

四、共享映射区(MMAP)

存储映射I/O

存储映射 I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。 于是当从缓冲区中取数据, 就相当于读文件中的相应字节。 于此类似, 将数据存入缓冲区, 则相应的字节就自动写入文件。 这样,就可在不适用 read 和 write 函数的情况下,使用地址(指针)完成 I/O 操作。使用这种方法, 首先应通知内核, 将一个指定文件映射到存储区域中。 这个映射工作可以通过 mmap 函数来实现。

img

mmap函数 

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
	addr: 	指定映射区的首地址。通常传【NULL】,表示让系统自动分配
	length:共享内存映射区的大小。(【<= 】文件的实际大小)
	prot:	共享内存映射区的读写属性。
        PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
	flags:	标注共享内存的共享属性。
        MAP_SHARED 修改会反映到磁盘上
        MAP_PRIVATE 修改不反映到磁盘上
	fd:	用于创建共享内存映射区的那个文件的 文件描述符。
	offset:默认0,表示映射文件全部。偏移位置。需是 【4k 的整数倍】。

返回:成功:返回创建的映射区首地址; 失败: MAP_FAILED:(void*(-1)) 宏

int munmap(void *addr, size_t length);
释放映射区。
	addr:mmap 的返回值
	length:大小

mmap使用注意事项!!!

1. 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。
2. 用于创建映射区的文件大小为 0,实际制定0大小创建映射区, 出 “无效参数”。
3. 用于创建映射区的文件读写属性为,只读。映射区属性为 读、写。 出 “无效参数”。
4. 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED时, mmap的读写权限,应该 <=文件的open权限。 只写不行。
5. 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用 地址访问。
6. offset 必须是 4096的整数倍。(MMU 映射的最小单位 4k )
7. 对申请的映射区内存,不能越界访问。
8. munmap用于释放的 地址,必须是mmap申请返回的地址。
9. 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。
10. 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可

mmap函数的保险调用方式:

fd = open("文件名", O_RDWR);
mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

父子进程间通信

#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

int var = 100;
int main(int argc,char*argv[]){

    int *p;

    int fd = open("temp",O_RDWR|O_CREAT|O_TRUNC, 0644);
    if(fd < 0){
        perror("open error");
        exit(1);
    }

    ftruncate(fd, 4);

    p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE,MAP_SHARED, fd , 0);
    if(p == MAP_FAILED){
        perror("mmap error");
        exit(1);
    }
    close(fd);

    pid_t pid = fork();

    if(pid == 0){
        *p = 2000;
        var = 1000;
        printf("p : %d,var: %d\n",*p,var);
    }else if(pid > 0){
        sleep(1);
        printf("p:%d,var:%d\n",*p,var);

        int ret = munmap(p,4);
        if(ret == -1){
            perror("munmap error");
            exit(1);
        }
    }else if(pid == -1){
        perror("fork error");
        exit(1);
    }
    return 0;
}

匿名映射mmap

匿名映射:只能用于 血缘关系进程间通信。
不用依赖构建映射文件
p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

无血缘关系进程间 mmap 通信

struct STU {  
     int id;  
     char name[20];  
     int age;  
};  
   
void sys_err(char *str)  {  
     perror(str);  
     exit(1);  
}  
   
int main(int argc, char *argv[])  
 {  
     int fd;  
     struct STU student = {10, "xiaoming", 30};  
     char *mm;  
   
     if (argc < 2) {  
        printf("./a.out file_shared\n");  
        exit(-1);  
     }  
  
     fd = open(argv[1], O_RDWR | O_CREAT, 0664);  
     ftruncate(fd, sizeof(student));  //拓展文件需要写权限
   
     mm = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);  
     if (mm == MAP_FAILED)  
         sys_err("mmap");  
   
     close(fd);  
   
     while (1) {  
         memcpy(mm, &student, sizeof(student));  
         student.id++;  
         sleep(1);  
     }  
   
     munmap(mm, sizeof(student));  
   
     return 0;  
 }  
struct STU{
   int id;
   char name[256];
   int age;
}

int main(int argc,char*argv[]){

    struct STU st;
    struct STU *p;

    int fd = open(argv[1], O_RDONLY);

    p = mmap(NULL, sizeof(st), PROT_READ, MAP_SHARED, fd, 0);

    close(fd);

    while(1){
        printf("id = %d\t name=%s\t %d", p->id,p->name,p->age);
        sleep(2);
    }

    munmap(p, sizeof(st));


    return 0;
}

mmap总结

1、创建映射区的过程中,隐含着一次对映射文件的【读操作】
2、当MAP_SHARED时,要求:映射区的权限应该<=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE(保证读一次)之后则无所谓,因为mmap中的权限是对内存的限制
3、映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭
4、特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!!mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。如,400字节大小的文件,在建立映射区时,offset 4096字节,则会报出总线错误
5、munmap传入的地址一定是mmap返回的地址。坚决【杜绝指针++】操作
6、文件偏移量必须为4K的整数倍
7、mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

五、信号

信号是信息的载体, Linux/UNIX 环境下,古老、经典的通信方式, 现下依然是主要的通信手段
信号共性:简单、不能携带大量信息、满足条件才发送。

信号机制

A 给 B 发送信号, B 收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。
信号的特质:由于信号是通过软件方法实现,每个进程收到的所有信号,都是由内核负责发送的,内核处理

与信号相关的事件

产生信号

1. 终端按键产生,如: Ctrl+c、 Ctrl+z、 Ctrl+\
Ctrl + c → 2) SIGINT(终止/中断) "INT" ----Interrupt
Ctrl + z → 20) SIGTSTP(暂停/停止) "T" ----Terminal 终端。
Ctrl + \ → 3) SIGQUIT(退出)

2. 系统调用产生,如: kill、 raise、 abort
3. 软件条件产生,如:定时器 alarm、周期定时setitimer
4. 硬件异常产生,如:非法访问内存(段错误)、除 0(浮点数例外)、内存对齐出错(总线错误)
除 0 操作 → 8) SIGFPE (浮点数例外) "F" -----float 浮点数。
非法访问内存 → 11) SIGSEGV (段错误)
总线错误 → 7) SIGBUS

5. 命令产生,如: kill 命令
kill 命令产生信号: kill -SIGKILL pid

未决:产生与递达之间状态。
递达:产生并且送达到进程。直接被内核处理掉。

信号的处理方式

1. 执行默认动作
2. 忽略(丢弃)
3. 捕捉(调用户处理函数)

阻塞信号集&未决信号集

Linux 内核的进程控制块 PCB 是一个结构体, task_struct, 除了包含进程 id,状态,工作目录,用户 id,组 id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集

阻塞信号集(信号屏蔽字): 将某些信号加入集合,对他们设置屏蔽,当屏蔽 x 信号后,再收到该信号,该信号的处理将推后(解除屏蔽后,直接被内核处理掉)。

未决信号集:
1. 信号产生,未决信号集中描述该信号的位立刻翻转为 1,表信号处于未决状态。当信号被处理对应位翻转回为 0。这一时刻往往非常短暂。
2. 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。

阻塞信号集(信号屏蔽字)
本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集
本质:位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。

信号四要素

每个信号必备 4 要素,分别是:1. 编号 2. 名称 3. 事件 4. 默认处理动作

使用 kill –l 命令查看当前系统可使用的信号
存在编号为 0 的信号。其中 1-31 号信号称之为常规信号(也叫普通信号或标准信号), 
34-64称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。

可通过 man 7 signal 查看帮助文档获取。也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h

在标准信号中,有一些信号是有三个“Value”,第一个值通常对 alpha 和 sparc 架构有效,中间值针对 x86、 arm和其他架构,最后一个应用于 mips 架构。一个‘-’表示在对应架构上尚未定义该信号。
“Action”:
Term:终止进程
Ign: 忽略信号 (默认即时对该种信号忽略操作)
Core:终止进程,生成 Core 文件。 (查验进程死亡原因, 用于 gdb 调试)
Stop:停止(暂停)进程
Cont:继续运行进程
特别强调: 9) SIGKILL 和 19) SIGSTOP 信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
另外需清楚, 只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!

Linux 常规信号一览表

1) SIGHUP: 用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号,默认动作为终止进程
2) SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
3) SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程
4) SIGILL: CPU 检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件
5) SIGTRAP:该信号由断点指令或其他 trap 指令产生。默认动作为终止进程 并产生 core 文件。
6) SIGABRT: 调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件。
7) SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件。
8) SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误。默认动作为终止进程并产生 core 文件。
9) SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
10) SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
11) SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生 core 文件。
12) SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
13) SIGPIPE: Broken pipe 向一个没有读端的管道写数据。默认动作为终止进程
14) SIGALRM: 定时器超时,超时的时间由系统调用 alarm 设置。默认动作为终止进程。
15) SIGTERM程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell 命令 Kill 时,缺省产生这个信号。默认动作为终止进程。
16) SIGSTKFLT: Linux 早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
17) SIGCHLD:子进程状态发生变化时,父进程会收到这个信号。默认动作为忽略这个信号。
18) SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
19) SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
20) SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
21) SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
22) SIGTTOU: 该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
23) SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
24) SIGXCPU:进程执行时间超过了分配给该进程的 CPU 时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
25) SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
26) SIGVTALRM:虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间。默认动作为终止进程。
27) SGIPROF:类似于 SIGVTALRM,它不公包括该进程占用 CPU 时间还包括执行系统调用时间。默认动作为终止进程。
28) SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
29) SIGIO:此信号向进程指示发出了一个异步 IO 事件。默认动作为忽略。
30) SIGPWR:关机。默认动作为终止进程。
31) SIGSYS:无效的系统调用。默认动作为终止进程并产生 core 文件。
34) SIGRTMIN ~ (64) SIGRTMAX: LINUX 的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。

kill函数

给指定进程发送指定信号(不一定杀死)

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

int kill(pid_t pid, int sig);
参数:
	pid: 	> 0:发送信号给指定进程
		= 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。
		< -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
		= -1:发送信号给,有权限发送的所有进程。
	signum:待发送的信号
返回值:
	成功: 0
	失败: -1 errno

父进程杀死指定子进程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>

int main(int argc,char*argv[]){

    int i = 0;
    pid_t pid,kpid;
    for(i = 0;i<5;i++){
        if((pid = fork())==0){
            break;
        }
        if(i==2){
            kpid = pid;
        }
    }

    if(i == 5){
        sleep(2);
        printf("parent pid :%d\n",getpid());
        printf("kill pid:%d\n",kpid);
        kill(kpid,SIGKILL);
    }else if(i!=5){
        printf("child%d pid: %d, parent pid:%d\n",i,getpid(),getppid());
        sleep(2);
    }
    return 0;
}

raise函数

#include <signal.h>

给当前进程发送指定信号(自己给自己发),等价于kill(getpid(), sig);
int raise(int sig);
参数:
    sig:指定信号
返回值:
    成功:0
    失败:非0值

abort函数

#include <stdlib.h>

给自己发送异常终止信号 6) SIGABRT,并产生core文件,等价于kill(getpid(), SIGABRT)
void abort(void);

alarm函数

使用自然计时法。定时发送SIGALRM给当前进程。每个进程都有且只有唯一的闹钟

#include <unistd.h>

unsigned int alarm(unsigned int seconds);
参数:	seconds:定时秒数
返回值:上次定时剩余时间。
	    无错误现象。

alarm(0):取消闹钟,返回旧闹钟余下秒数。

例: 调用alarm(5) → 程序运行3sec,此时返回值为2sec → 同时调用alarm(4) → 程序运行5sec ,此时返回值为0 → 调用alarm(5) → 同时调用alarm(0)取消定时器,返回值为5sec
定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸...无论进程处于何种状态,alarm 都计时。

测试一秒钟计数

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc,char*argv[]){
    alarm(1);
    int i = 0;

    while(1){
        i++;
        printf("%d\n",i);
    }
    return 0;
}

使用time命令查看程序执行的时间。程序运行的瓶颈在于IO,优化程序,首选优化IO
实际执行时间 = 系统时间 + 用户时间 + 等待时间

setitimer 函数

设置定时器(闹钟)。 可代替 alarm 函数。精度微秒 us,可以实现周期定时。

#include <sys/time.h>

获取定时时长
int getitimer(int which,struct itimerval *curr_value);

设置定时时长
int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);
参数:
	which:	ITIMER_REAL: 采用自然计时。 ——> SIGALRM
			ITIMER_VIRTUAL: 采用用户空间计时  ---> SIGVTALRM
			ITIMER_PROF: 采用内核+用户空间计时 ---> SIGPROF
	new_value:定时秒数
	old_value:传出参数,上次定时剩余时间。
返回值:
	成功: 0
	失败: -1 errno

struct itimerval {
    struct timeval {
        time_t      tv_sec;         /* seconds */
        suseconds_t tv_usec;        /* microseconds */
    }it_interval;---> 用于设定两个定时任务之间的间隔时间
    struct timeval {
        time_t      tv_sec;         
        suseconds_t tv_usec;        
    }it_value;  ---> 第一次定时秒数
};

可以理解为有2个定时器
一个用于定时什么时候触发打印
一个用于之后间隔多少时间再次触发闹钟

使用setitimer定时,向屏幕打印信息

信号集操作函数!!!

内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字 mask 可以影响未决信号集。而我们可以在应用程序中自定义 set 来改变 mask。已达到屏蔽指定信号的目的。

#include <signal.h>

自定义信号集
sigset_t set; // typedef unsigned long sigset_t;

int sigemptyset(sigset_t *set); 
将某个信号集清 0 成功:0;失败:-1

int sigfillset(sigset_t *set); 
将某个信号集置 1 成功:0;失败:-1

int sigaddset(sigset_t *set, int signum); 
将某个信号加入信号集 成功:0;失败:-1

int sigdelset(sigset_t *set, int signum); 
将某个信号移除信号集 成功:0;失败:-1

int sigismember(const sigset_t *set, int signum);
判断某个信号是否在信号集中 返回值:在集合:1;不在:0;出错:-1

注:sigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。

sigprocmask 函数!!!

用来屏蔽信号、 解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB 中)
严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
set:传入参数,是一个位图, set 中哪位置 1,就表示当前进程屏蔽哪个信号。
oldset:传出参数,保存旧的信号屏蔽集。
how 参数取值: 假设当前的信号屏蔽字为 mask
    1. SIG_BLOCK: 当 how 设置为此值, set 表示需要屏蔽的信号。
       相当于 mask = mask|set
    2. SIG_UNBLOCK: 当 how 设置为此, set 表示需要解除屏蔽的信号。
       相当于 mask = mask & ~set
    3. SIG_SETMASK: 当 how 设置为此, set 表示用于替代原始屏蔽及的新屏蔽集。
       相当于 mask = set
    若,调用 sigprocmask 解除了对当前若干个信号的阻塞,则在 sigprocmask 返回前,
至少将其中一个信号递达。

sigpending 函数

取当前进程的未决信号集

#include <signal.h>

int sigpending(sigset_t *set);
参数:set 传出参数。 
返回值:成功: 0;失败: -1,设置 errno

练习:把所有常规信号的未决状态打印至屏幕

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <pthread.h>

void print_set(sigset_t *set){
    int i;

    for(i =1;i<32;i++){
        if(sigismember(set,i)){
            putchar('1');
        }else{
            putchar('0');
        };
    }
    printf("\n");
}

int main(int argc,char *argv[]){

    sigset_t set, oldset, myset;

    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    int ret = sigprocmask(SIG_BLOCK,&set,&oldset);
    if(ret == -1){
        perror("sigprocmask error");
        exit(1);
    }
    while(1){
        sigpending(&myset);
        print_set(&myset);
        sleep(1);
    }

    return 0;
}

信号捕捉

signal函数

注册一个信号捕捉函数

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
参数:
    signum :待捕捉信号
    handler:捕捉信号后的操纵函数,函数返回值为void,形参必须为int
返回值:
    成功:返回操纵函数的返回值
    失败:返回SIG_ERR

sigaction 函数!!!

修改信号处理动作(通常用来注册一个信号的捕捉函数)

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
参数:
    signum:捕捉信号
    act:传入参数,新的处理方式。
    oldact:传出参数,旧的处理方式。
返回值:
    成功:0
    失败:-1,设置errno

struct sigaction {
    void (*sa_handler)(int);  //只装状态信息
    void (*sa_sigaction)(int, siginfo_t *, void *); //携带更多信息
    sigset_t sa_mask; //只作用于函数捕捉期间,暂时屏蔽mask作用于全局
    int sa_flags;
    void (*sa_restorer)(void);
};

sa_restorer:该元素是过时的,不应该使用, POSIX.1 标准将不指定该元素。 (弃用)
sa_sigaction:当 sa_flags 被指定为 SA_SIGINFO 标志时,使用该信号处理程序。 (很少使用)
重点掌握:
<1> sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为 SIG_IGN 表忽略 或 SIG_DFL 表执行默认动作
<2> sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
<3>  sa_flags:通常设置为 0,表示使用默认属性,即XXX 信号捕捉函数执行期间, XXX 信号自动被屏蔽

练习:sigaction捕捉SIGCHLD,回收子进程

补充细节:SIGCHLD信号

SIGCHLD 的产生条件:
<1> 子进程终止时
<2> 子进程接收到 SIGSTOP 信号停止时
<3> 子进程处在停止态,接受到 SIGCONT 后唤醒

SIGCHLD 信号注意问题:
1. 子进程继承父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集 spending。
2. 注意注册信号捕捉函数的位置。
3. 应该在 fork 之前,阻塞 SIGCHLD 信号。注册完捕捉函数后解除阻塞。

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

void catch_signal(int signum){
    pid_t wpid;
    int status;

    while((wpid = waitpid(-1,&status,0))!=-1){
        if(WIFEXITED(status)){
            printf("catch child id:%d, ret:%d\n",wpid,WEXITSTATUS(status));
        }
    }
}

int main(int argc,char*argv[]){

    pid_t pid;

    sigset_t set, oldset;

    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, &oldset);

    int i;
    for(i =0;i<5;i++){
        if((pid = fork()) == 0){
            break;
        }
    }

    if(5 == i){
        struct sigaction act;

        act.sa_handler = catch_signal;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;

        sigaction(SIGCHLD, &act, NULL);

        int ret = sigprocmask(SIG_UNBLOCK, &set, &oldset);
        if(ret == -1){
            perror("sigprocmask error");
            exit(1);
        }

        printf("parent pid:%d\n",getpid());

        while(1);
    }else{
        sleep(1);
        printf("child pid:%d\n",getpid());
        return i;
    }
    return 0;
}                          

信号捕捉特性

信号捕捉特性:
1. 捕捉函数执行期间,信号屏蔽字归sa_mask控制 , 捕捉函数执行结束。 恢复回mask
2. 捕捉函数执行期间,本信号自动被屏蔽(sa_flgs = 0).其他信号不屏蔽,如需屏蔽则调用sigsetadd函数修改
3. 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!

中断系统调用

系统调用可分为两类:慢速系统调用和其他系统调用。
1. 慢速系统调用:可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如, read、 write、 pause、 wait...
2. 其他系统调用: getpid、 getppid、 fork...

解决慢速系统调用中断

可修改 sa_flags 参数来设置被信号中断后系统调用是否重启。 SA_INTERRURT 不重启SA_RESTART 重启

六、信号量

信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据

semget函数

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

创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int nsems, int semflg);
参数:
    key:提供一个键,非血缘关系的进程可以通过它访问同一个信号量
    nsems:代表该内核对象中,信号量的个数
    semflg:O_CREAT | O_EXCL
返回值:
    成功:信号量标识符
    失败:-1,errno

semop函数

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

对信号量组进行操作,改变信号量的值
int semop(int semid, struct sembuf *sops, size_t nsops);
参数:
    semid:由semget返回信号量的标识符
    sops:指向信号量操作数组
        struct sembuf{
            short sem_num; //信号量编号,使用单个信号量时,通常取0
            short sem_op;  //信号量操作:取值为-1:P操作,为+1表示V操作
            short sem_flg; //通常设置为SEM_UNDO。这样在进程没释放信号量而退出时,
                             系统自动释放该进程中未释放的信号量   
        }
    nsops:操作数组sops中的操作个数,通常取1(一个操作)
返回值:
    成功:信号量标识符,在信号量的其他函数中都会使用该值
    失败:-1

semctl函数

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

控制信号量的相关信息
int semctl(int semid, int semnum, int cmd, ...);
参数:
    semid:由semget返回信号量的标识符
    semnum:要操作的信号量数组的信号量下标
    cmd:指定对信号量的各种操作
        PC_STAT 查询此信号量数组的数据存入arg.buf(buf为struct semid_ds结构体指针)
        IPC_RMID 删除指定semid的信号量数组
        GETVAL 获取信号量的当前值,
        SETVAL 设置信号量的值,初始化要用的命令

练习:进程间通信

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
 
//       int semget(key_t key, int nsems, int semflg);
//       int semctl(int semid, int semnum, int cmd, ...);
//       int semop(int semid, struct sembuf *sops, size_t nsops);
union semun{
        int              val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
};
 
//P操作,拿钥匙
void PGetKey(int semid)
{
        struct sembuf sops;
        sops.sem_num = 0;
        sops.sem_op = -1;
        sops.sem_flg = SEM_UNDO;
 
        semop(semid, &sops, 1);
}
 
//V操作,放回钥匙
void VPutBackKey(int semid)
{
        struct sembuf sops;
        sops.sem_num = 0;
        sops.sem_op = 1;
        sops.sem_flg = SEM_UNDO;
 
        semop(semid, &sops, 1);
}
 
int main()
{
        key_t key;
        int semid;
        if((key == ftok(".",6)) < 0)
        {
                printf("ftok error\n");
        }
 
        semid = semget(key , 1,  IPC_CREAT|0666);//创造钥匙,数量为1
 
        union semun sem;
        sem.val = 0;//初始状态为没有钥匙
        semctl(semid, 0, SETVAL, sem);//SETVAL初始化信号量为一个已知的值,这时就需要第四个参数
                     //0表示操作第一把钥匙
        int pid = fork();
 
        if(pid < 0)
        {
                printf("fork failed\n");
        }else if(pid == 0)
        {
                printf("this is child\n");
                VPutBackKey(semid);//首先把钥匙放回     
        }else
        {
                PGetKey(semid);//等子进程将钥匙放回后才会进行操作,保证子进程先执行
                printf("this is father\n");
                VPutBackKey(semid);
                semctl(semid, 0, IPC_RMID);//销毁钥匙
        }
 
        return 0;
}

七、消息队列

消息队列,是消息的链接表,存放在内核之中。一个消息队列由一个标识符(即队列ID)来标识。
用户进程可以向消息队列添加消息,也可以向消息队列读取消息

ftok函数

该函数的作用是生成一个具有唯一性的ID。这里的ID不是消息队列的ID,因为消息队列是一份临界资源,为了避免该消息队列被无关线程访问,所以设置了房间密码,ftok的返回值则是 “ 房间密码 ”

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

key_t ftok(const char *pathname, int proj_id);
参数:
    pathname:路径名
    proj_id:项目ID
返回值:
    成功:“房间密码”
    失败:-1

注意:ftok被不同进程调用,只要路径名和ID是一样的,生成的整数就是一样的

msgget函数

通过ftok得到的房间密码或自定义密码(即(key_t)1234)创建一个消息队列 或者 拿到已有消息队列的ID。

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

int msgget(key_t key, int msgflg);
参数:
    key:房间密码,可由ftok,也可由自定义(key_t) 1234;
    msgflag:创建消息队列的方式,同时设置权限
        IPC_CREAT:可以单独使用,如果共享内存不存在,则重新开辟,函数返回值是新开辟的
        共享内存的ID;如果已经存在,则沿用已有的共享内存,函数返回值是已有的共享内存的ID
        IPC_EXCL:无法单独使用,要配合IPC_CREAT使用,即 IPC_CREAT | IPC_EXCL
        IPC_CREAT | IPC_EXCL:如果共享内存不存在,则重新开辟,函数返回值是新开辟的共享内存
        的ID;如果已经存在,则报错
        IPC_CREAT | IPC_EXCL | 0664:开辟共享内存的同时,设置共享内存的访问权限

可以通过命令行输入ipcs来判断消息队列是否创建成功

msgsnd函数

向消息队列中添加一条消息

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

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
    msqid:消息队列ID,即msgget函数的返回值
    msgp:消息缓冲区的地址,添加消息一定满足当下的格式,传入msgbuf
    msgsz:表示消息长度,这里的消息长度指的是上面这个结构体中buf成员所占字节数
    msgflg:表示发送消息的方式
            0	当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
            IPC_NOWAIT	当消息队列已满的时候,msgsnd函数不等待立即返回,此时是以报错的形式返回。
返回值:成功:0
       失败:-1

struct msgbuf {
    long mtype;       // 消息类型,在从消息队列取出消息的时候,可以根据消息类型来选择性地取消息   
    char mtext[1];    // 消息内容
                      //(结构体的最后一个成员是数组,该数组也被称为vector,即数组大小可变)            
};

msgrcv函数

从消息队列的队头取出一条消息

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

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
    msgid:消息队列id
    msgp:输出型参数,表示消息缓冲区的地址
    size:用于存放接收到的消息数据的缓冲区大小
    msgtype:选择想要取出的消息类型
        = 0	直接取出队头消息,不论是什么类型
        > 0	从所有消息类型为msgtype的消息中,取出其中的第一条消息
        < 0	接收类型小于等于 msgtype的绝对值的第一条消息。 
    msgflag:表示接收消息的方式
        0	阻塞式接收消息
        IPC_NOWAIT	如果消息队列中没有消息,则立马返回,此时错误码为ENOMSG
        MSG_EXCEPT	与msgtype配合使用,返回队列中第一个类型不为msgtype的消息   

补充:针对msgtype < 0做一些补充,假设消息队列里的消息类型有 1、3、4、5 四种类型的消息,
如果msgtype = -4,绝对值是4,那就需要取出消息类型小于等于4的所有消息,因此,取出消息的
消息类型必须是1、3、4类型。

msgctl函数

控制消息队列,可以用来销毁消息队列

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

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
    msgid:消息队列id
    cmd:对消息队列执行的具体操作,如拷贝、查询、销毁等。
        IPC_STAT	将消息队列的相关信息拷贝放到第三个参数 buf 中
        IPC_RMID	销毁消息队列
    buf:消息队列缓冲区。比如 cmd 为IPC_STAT时,会把消息队列的相关信息拷贝到该缓冲区中

struct msqid_ds {
      struct ipc_perm msg_perm;     /* Ownership and permissions */
      time_t          msg_stime;    /* Time of last msgsnd(2) */
      time_t          msg_rtime;    /* Time of last msgrcv(2) */
      time_t          msg_ctime;    /* Time of last change */
      unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
      msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
      msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
      pid_t           msg_lspid;    /* PID of last msgsnd(2) */
      pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};

struct ipc_perm {
      key_t          __key;       /* Key supplied to msgget(2) */
      uid_t          uid;         /* Effective UID of owner */
      gid_t          gid;         /* Effective GID of owner */
      uid_t          cuid;        /* Effective UID of creator */
      gid_t          cgid;        /* Effective GID of creator */
      unsigned short mode;        /* Permissions */
      unsigned short __seq;       /* Sequence number */
};

练习:消息队列进程间通信

发送进程

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

#define SENDBUF     1024

typedef struct msgbuf {
    long mtype;       /* message type, must be > 0 */
    char mtext[SENDBUF];    /* message data */
}msgq_t;

const key_t g_msg_key = 1234;

int main(int argc, char* argv[])
{
    int msg_handle, ret, run_flag = 1;
    msgq_t data;
    char sendbuf[SENDBUF];
    long int msgtype = 1;

    msg_handle = msgget(g_msg_key, IPC_CREAT | IPC_EXCL | 0664);
    if (msg_handle == -1)
    {
        printf("msgget failed with error %d\n", errno);
        exit(EXIT_FAILURE);
    }

    while (run_flag)
    {
        printf("Please input msgtype and msg\n");
        scanf("%ld%s", &msgtype, sendbuf);

        data.mtype = msgtype;
        strcpy(data.mtext, sendbuf);

        ret = msgsnd(msg_handle, &data, sizeof(msgq_t) - sizeof(long), 0);
        if(ret == -1) {
            printf("msgsnd failed with error %%s\n", strerror(errno));
            exit(EXIT_FAILURE);
        }

        if (strncmp(sendbuf, "end", 3) == 0)
        {
            run_flag = 0;
        }
    }
    
    ret = msgctl(msg_handle, IPC_RMID, 0);
    if(ret == -1) {
        printf("msgctl failed with error %s\n", strerror(errno));
        exit(EXIT_FAILURE);    
    }

    return 0;
}

接收进程

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>

typedef struct msgbuf {
    long mtype;       /* message type, must be > 0 */
    char mtext[BUFSIZ];    /* message data */
}msgq_t;

const key_t g_msg_key = 1234;

int main(int argc, char* argv[])
{
    int msg_handle, ret;
    int run_flag = 1;
    long int msgtype = -5;
    msgq_t data;

    msg_handle = msgget(g_msg_key, IPC_CREAT);
    if(msg_handle == -1) {
        printf("msgget failed with error %d\n", errno);
        exit(EXIT_FAILURE);
    }

    while (run_flag)
    {
        ret = msgrcv(msg_handle, &data, BUFSIZ, msgtype, 0);
        if(ret == -1) {
            printf("msgrcv failed with error %d\n", errno);
            exit(EXIT_FAILURE);
        } else {
            printf("recv queue type %ld, data: %s\n", data.mtext, data.mtext);
        }
        if(strncmp(data.mtext, "end", 3) == 0) {
            run_flag = 0;
        }
    }
    
    ret = msgctl(msg_handle, IPC_RMID, 0);
    if(ret == -1) {
        printf("msgctl failed with error %s\n", strerror(errno));
        exit(EXIT_FAILURE);    
    }

    return 0;
}

总结:

管道:速度慢,容量有限,只有父子进程能通讯;
FIFO:任何进程间都能通讯,但速度慢;
消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;
共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题;
信号:只能在父子进程,开销小;
信号量:不能传递复杂消息,只能用来同步。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值