目录
在进程间进行数据传递需要借助操作系统提供的特殊方法,常用的进程间通信方式有:
1. 管道:使用简单
2. 信号:开销小
3. mmap映射:非血缘关系进程间
4. socket(本地套接字):稳定
管道
最基本的IPC机制,作用于有血缘关系的进程之间完成数据传递,调用pipe系函数可创建管道。
实现原理: 内核借助环形队列机制,使用内核缓冲区(4K)实现。
特质:
1. 伪文件
2. 管道中的数据只能一次读取。
3. 数据在管道中,只能单向流动。
局限性:
1. 数据不能进程自己写自己读。
2. 数据不可以反复读。
3. 半双工通信。
4. 只有血缘关系进程间可用。
pipe函数
原型: int pipe(int pipefd[2]);
创建并打开管道
参数:fd[0] 读端 fd[1] 写端
返回值:成功返回0 失败返回-1 errno
管道通信代码示例,父进程往管道写,子进程从管道读并打印读取内容:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int ret;
int fd[2];
pid_t pid;
char *str = "hello pipe\n";
char buf[1024];
ret = pipe(fd);
if(ret == -1) sys_err("pipe error");
pid = fork();
if(pid>0){
close(fd[0]); //父进程关闭读段
write(fd[1], str, strlen(str));
close(fd[1]);
sleep(1);
} else if (pid == 0){
close(fd[1]); //子进程关闭写段
ret = read(fd[0], buf, sizeof(buf));
write(STDOUT_FILENO, buf, ret);
close(fd[0]);
}
return 0;
}
管道的读写行为:
读管道:
1. 管道有数据,read返回实际读到的字节数
2. 管道无数据: 无写端——read返回0(类似读到文件尾);有写端——read阻塞等待
写管道:
1. 管道无读端:异常终止(SIGPIPE信号导致)
2. 有读端:管道已满——阻塞等待;管道未满——返回写出字节个数
使用管道实现父子进程读写完成ls|wc -l效果:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int fd[2];
int ret;
pid_t pid;
ret = pipe(fd);
if(ret==-1){
sys_err("pipe error");
}
pid = fork();
if(pid == -1){
sys_err("fork error");
}else if (pid > 0){
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc","wc","-l", NULL);
}else if (pid == 0){
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls","ls",NULL);
}
return 0;
}
管道实现兄弟进程间通信,使用循环创建兄弟进程,用循环因子标示,兄:ls 弟: wc -l
注意:父进程在fork之后,依然把持着管道的读端和写端,因此需要先close掉父进程的读写来保证管道信息的单向流动。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/wait.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int fd[2];
int ret, i;
pid_t pid;
ret = pipe(fd);
if(ret==-1){
sys_err("pipe error");
}
for(i=0;i<2;i++){
pid = fork();
if(pid==-1) sys_err("fork error");
if(pid==0) break;
}
if(i==2){
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
} else if(i == 0){ //brother
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls","ls",NULL);
} else if(i == 1){
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc","wc","-l", NULL);
}
return 0;
}
命名管道FIFO
mkfifo函数
原型: int mkfifo(const cahr *pathname, mode_t mode);
可用于无血缘关系进程间的通信,操作方式和文件类似
创建FIFO管道:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(){
int ret = mkfifo("testfifo", 0664);
return 0;
}
FIFO实现非血缘关系进程间通信:
写端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
void sys_err(char *str){
perror(str);
exit(-1);
}
int main(int argc, char *argv[]){
int fd, i;
char buf[4096];
if(argc < 2){
printf("Enter the fifoname\n");
return -1;
}
fd = open(argv[1], O_WRONLY);
if(fd < 0) sys_err("open");
i = 0;
while(1){
sprintf(buf, "hello itcast %d\n", i++);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
读端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
void sys_err(char *str){
perror(str);
exit(-1);
}
int main(int argc, char *argv[]){
int fd, i;
char buf[4096];
if(argc < 2){
printf("Enter the fifoname\n");
return -1;
}
fd = open(argv[1], O_RDONLY);
if(fd < 0) sys_err("open");
while(1){
int len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
sleep(1);
}
close(fd);
return 0;
}
开启后通信如下
存储映射I/O
存储映射I/O(Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用read和write函数的情况下,使地址指针完成I/O操作。
mmap函数
函数原型: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,设置errno
munmap函数
原型:int munmap(void *addr, size_t length);
释放映射区,参数addr传入mmap返回值,length为大小
示例,使用mmap创建一个映射区(共享内存),并往映射区里写入内容:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
char *p = NULL;
int fd;
fd = open("testmap", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd==-1){
sys_err("open error");
}
ftruncate(fd, 20);
int len = lseek(fd,0,SEEK_END);
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){
sys_err("mmap error");
}
strcpy(p, "hello mmap");
printf("---%s\n", p);
int ret = munmap(p,len);
if(ret == -1){
sys_err("munmap error");
}
return 0;
}
mmap注意事项:
1. 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。
2. 用于创建映射区的文件大小为 0,实际制定0大小创建映射区, 出 “无效参数”。
3. 用于创建映射区的文件读写属性为,只读。映射区属性为 读、写。 出 “无效参数”。
4. 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED时, mmap的读写权限,应该 <=文件的open权限。 只写不行。
5. offset需要是4096的整数倍。
6. 对申请的映射区内存不能越界访问。
7. 文件描述符fd在mmap创建映射区完成即可关闭(后续访问文件使用地址)。
8. munmap用于释放的 地址,必须是mmap申请返回的地址。
9. 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。
10. 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可。
mmap函数保险调用方法:
1.open("文件名",O_RDWR)
2.mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0)
父子进程mmap通信:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int *p = 0;
int var = 0;
int fd;
pid_t pid;
fd = open("tmp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd==-1){
sys_err("open error");
exit(1);
}
ftruncate(fd, 4);
//p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){
sys_err("mmap error");
exit(1);
}
close(fd);
pid = fork();
if(pid == 0){
*p = 7000;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
} else{
sleep(1);
printf("parent, *p=%d, var = %d\n", *p, var);
wait(NULL);
int ret = munmap(p, 4);
if(ret == -1){
sys_err("munmap error");
exit(1);
}
}
return 0;
}
全局变量(读时共享,写时复制特性),子进程对*p的修改反映到了父进程上:
将共享内存定义为private则结果如下:
使用mmap实现无血缘关系进程间通信:
写端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
struct student{
int id;
char name[256];
int age;
};
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int fd;
struct student stu={1,"jack",18};
struct student *p;
fd = open("tmp", O_RDWR|O_CREAT|O_TRUNC, 0664);
if(fd==-1){
sys_err("open error");
exit(1);
}
ftruncate(fd, sizeof(stu));
p = mmap(NULL, sizeof(stu), PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){
sys_err("mmap error");
exit(1);
}
close(fd);
while(1){
memcpy(p,&stu,sizeof(stu));
stu.id++;
sleep(1);
}
munmap(p,sizeof(stu));
return 0;
}
读端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <fcntl.h>
struct student{
int id;
char name[256];
int age;
};
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc, char *argv[]){
int fd;
struct student stu={1,"jack",18};
struct student *p;
fd = open("tmp", O_RDONLY);
if(fd==-1){
sys_err("open error");
exit(1);
}
p = mmap(NULL, sizeof(stu), PROT_READ, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){
sys_err("mmap error");
exit(1);
}
close(fd);
while(1){
printf("id = %d, name = %s, age = %d\n", p->id, p->name, p->age);
sleep(1);
}
return 0;
}
结果如下:
匿名映射:只能用于血缘关系进程间通信。
p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
关于匿名映射作用可参照:匿名映射的作用_我是小x的博客-优快云博客_匿名映射
信号
信号共性:
简单、不能携带大量信息、满足条件才发送。
信号的特质:
信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。
所有信号的产生及处理全部都是由【内核】完成的。
信号相关的概念
产生信号:
1. 按键产生: Ctrl+c, Ctrl+z, Ctrl+\
2. 系统调用产生: kill, raise, abort
3. 软件条件产生:alarm
4. 硬件异常产生:非法访问内存(segment fault),除0,内存对齐出错(总线错误)
5. 命令产生:kill命令
未决:产生与递达之间状态,主要由于阻塞导致。
递达:产生并且送达到进程,直接被内核处理掉。
信号处理方式: 执行默认处理动作、忽略(丢弃)、捕捉(自定义用户处理函数)
阻塞信号集(信号屏蔽字): 本质:位图。用来记录信号的屏蔽状态。
屏蔽X信号再次收到时,在解除屏蔽前,该信号的处理将推后(一直处于未决态)。
未决信号集:本质:位图。用来记录信号的处理状态。
信号产生后,未决信号集中描述该信号的位翻转为1表示该信号处于未决态,被处理后翻转为0。该信号集中的信号表示已经产生,但尚未被处理。
信号四要素
1.编号 2. 名称 3.事件 4.默认处理方式
kill函数
原型:int kill(pid_t pid, int signum)
参数:
pid:
> 0:发送信号给指定进程
= 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。
< -1: 取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
= -1:发送信号给,有权限发送的所有进程。
signum:待发送的信号
返回值:成功: 0 失败: -1 设置errno
kill -9 -groupname 杀死一个进程组
示例,子进程发送信号kill父进程:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
int main(){
pid_t pid = fork();
if(pid>0){
printf("parent, pid=%d\n", getpid());
while(1);
} else if(pid==0){
printf("child pid = %d, ppid= %d\n", getpid(), getppid());
sleep(2);
kill(getppid(), SIGKILL);
}
return 0;
}
结果如下:
alarm函数
原型:unsigned int alarm(unsigned int seconds);
设置定时器(闹钟),指定seconds后,内核会给当前进程发送SIGALRM信号,进程收到该信号,默认动作终止。每个进程有且只有一个定时器。
参数:
seconds:定时秒数
返回值:上次定时的剩余时间(秒数) alarm(0)取消定时器
使用alarm(1)加while循环可以统计计算机一秒能打印多少个数字,例子略。
使用time命令查看程序执行时间
实际时间 = 用户时间 + 内核时间 + 等待时间 —— 程序优化的瓶颈在于IO
setitimer函数
设置定时器,可代替alarm函数,精度微秒,可以实现周期定时
原型: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:定时秒数
类型: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; ---> 第一次定时秒数
};
old_value:传出参数,上次定时剩余时间。
示例,使用setitimer函数定时每五秒打印输出到屏幕:
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
void myfunc(int signo){
printf("hello world\n");
}
int main(void){
struct itimerval it, oldit;
signal(SIGALRM,myfunc);
it.it_value.tv_sec = 2;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 5;
it.it_interval.tv_usec = 0;
if(setitimer(ITIMER_REAL, &it, &oldit) == -1){
perror("setitimer error");
return -1;
}
while(1)
return 0;
}
信号集操作函数
1. 自定义信号集的设定
sigset_t set; 自定义信号集。
sigemptyset(sigset_t *set); 清空信号集
sigfillset(sigset_t *set); 全部置1
sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中
sigdelset(sigset_t *set, int signum); 将一个信号从集合中移除
sigismember(const sigset_t *set,int signum); 判断一个信号是否在集合中,在返回1不在返回0
2. 设置信号屏蔽字和解除屏蔽
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how: SIG_BLOCK: 设置阻塞
SIG_UNBLOCK: 取消阻塞
SIG_SETMASK: 用自定义set替换mask
set: 自定义set
oldset:旧有的 mask
3. 查看未决信号集
int sigpending(sigset_t *set);
set: 传出的未决信号集
信号列表:
示例,利用自定义集合设置指定信号的阻塞:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
void print_set(sigset_t *pedset){
int i;
for(i=1;i<32;i++){
if(sigismember(pedset, i))
putchar('1');
else
putchar('0');
}
printf("\n");
}
int main(){
int ret = 0;
sigset_t set,oldset,pedset;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, &oldset);
while(1){
ret = sigpending(&pedset);
if(ret==-1)
sys_err("sigpending error");
print_set(&pedset);
}
return 0;
}
编译运行,在输入Ctrl+C之后,进程捕捉到信号,但由于设置阻塞,没有处理,未决信号集对应位置变为1
信号捕捉
signal函数
注册一个信号捕捉函数
原型:
typedef void(*sighandler_t)(int);
sighandler signal(int signum, sighandler_t handler);
参数:signum - 待捕捉信号 handler - 捕捉信号后的操作函数
返回值:
示例,捕捉Ctrl+c信号:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
void sig_catch(int signo){
printf("catch you%d\n", signo);
return;
}
int main(){
signal(SIGINT, sig_catch);
while(1);
return 0;
}
结果:
sigaction函数
原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:待捕捉信号,结构体struct action
sa_handler:指定信号捕捉后的处理函数名(即注册函数),也可复制为SIG_IGN表忽略或SIG_DFL表执行默认动作
sa_mask:调用信号处理函数时所要屏蔽的信号集合(信号屏蔽字),仅在处理函数被调用期间屏蔽生效。
sa_flags:通常设置为0,表使用默认属性
返回值:
示例,使用sigaction函数捕捉两种信号:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
void sig_catch(int signo){
if(signo == SIGINT){
printf("catch you%d\n", signo);
}else if(signo == SIGQUIT){
printf("----catch you%d\n",signo);
}
return;
}
int main(int argc, char* argv[]){
struct sigaction act, oldact;
act.sa_handler = sig_catch; //set callback function name
sigemptyset(&(act.sa_mask)); //set mask when sig_catch working
act.sa_flags = 0; //default
int ret = sigaction(SIGINT, &act, &oldact);
if(ret == -1){
sys_err("sigaction error");
}
ret = sigaction(SIGQUIT, &act, &oldact);
while(1);
return 0;
}
结果如下:
信号捕捉的特性:
内核实现信号捕捉过程:
SIGCHLD信号
产生条件:子进程状态发生变化时产生
1. 子进程终止时 2.子进程接收SIGSTOP停止时 3. 静止态收到SIGCONT后唤醒时
示例,使用信号捕捉回收子进程:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
void sys_err(const char* str){
perror(str);
exit(1);
}
void catch_child(int signo){
pid_t wpid;
int status;
//while((wpid = wait(NULL))!=-1)
while((wpid = waitpid(-1, &status, 0))!= -1){ // clear zombie procedure
if(WIFEXITED(status)){
printf("catch child id %d, ret=%d\n", wpid, WEXITSTATUS(status));
}
}
return;
}
int main(){
pid_t pid;
// set block
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
int i;
for(i=0; i<15; i++){
if((pid=fork())==0)
break;
}
if(15==i){
struct sigaction act;
act.sa_handler = catch_child; //set callback function
sigemptyset(&act.sa_mask); //set mask when doing catch
act.sa_flags = 0; //set default
sigaction(SIGCHLD, &act, NULL);
// unblock
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("I'm parent,pid = %d\n", getpid());
while(1);
} else{
printf("I'm child pid = %d\n",getpid());
return i;
}
return 0;
}
通过循环wait是因为多个进程同时死亡,由于相同信号的不排队原则,父进程只会去处理累积信号中的一个,因此循环回收防止僵尸进程出现。
还有一种情况是父进程还没注册完捕捉函数,子进程就死亡了,解决的办法是在int i之前设置屏蔽,等父进程注册完捕捉函数再解除,这样即使子进程死亡,信号也因为被屏蔽而无法到达父进程,解除屏蔽过后,父进程就能处理累积起来的信号了。
中断系统调用(慢速)
系统调用可分为慢速系统调用和其他系统调用
慢速系统调用:可能会使进程永久阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期),也可以设定系统调用是否重启。如read, write, pause…
其他系统调用:如getpid,getppid,fork等