目录
练习:父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,当输入quit结束。
【1】进程间通信 IPC
1》 进程间通信方式
(1)早期的进程间通信:
无名管道(pipe)、有名管道(fifo)、信号(signal)
(2)system V PIC:
共享内存(share memory)、信号灯集(semaphore)、消息队列(message queue)
(3)BSD:
套接字(socket)
2》 无名管道
1> 特点
(1)只能用于具有亲缘关系的进程之间的通信
(2)半双工的通信模式,具有固定的读端fd[0]和写端fd[1]。
(3)管道可以看成是一种特殊的文件,对于它的读写可以使用文件IO如read、write函数。
(4)管道是基于文件描述符的通信方式。当一个管道建立时,它会创建两个文件描述符 fd[0]和fd[1]。其中fd[0]固定用于读管道,而fd[1]固定用于写管道。
2> 函数接口
int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端 fd[1]:写端
返回值:成功 0
失败 -1
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
char buf[65536] = "";
int fd[2] = {0}; //fd[0]代表读端,fd[1]代表写端
if (pipe(fd) < 0)
{
perror("pipe err");
return -1;
}
printf("%d %d\n", fd[0], fd[1]);
//结构类似队列,先进先出
//1. 当管道中无数据时,读阻塞。
// read(fd[0], buf, 32);
// printf("%s\n", buf);
//但是关闭写端就不一样了
//当管道中有数据关闭写端可以读出数据,无数据时关闭写端读操作会立即返回。
// write(fd[1], "hello", 5);
// close(fd[1]);
// read(fd[0], buf, 32);
// printf("%s\n", buf);
//2. 当管道中写满数据时,写阻塞,管道空间大小为64K
// write(fd[1], buf, 65536);
// printf("full!\n");
//write(fd[1], "a", 1); //当管道写满时不能再继续写了会阻塞
//写满一次之后,当管道中至少有4K空间时(也就是读出4K),才可以继续写,否则阻塞。
// read(fd[0], buf, 4096); //换成4095后面再写就阻塞了,因为不到4K空间
// write(fd[1], "a", 1);
//3. 当读端关闭,往管道中写入数据无意义,会造成管道破裂,进程收到内核发送的SIGPIPE信号。
close(fd[0]);
write(fd[1], "a", 1);
printf("read close\n");
return 0;
}
用gdb调试可以看见管道破裂信号:
gcc -g xx.c
gdb a.out
r
3> 注意事项
(1)当管道中无数据时,读操作会阻塞
管道中有数据,将写端关闭,可以将数据读出
管道中无数据,将写端关闭,读操作会立即返回
(2)管道中装满(管道大小64K)数据写阻塞,一旦由 4k 空间,写继续
(3)只有在管道的读端存在时,向管道中写入数据才有意义,否则,会导致管道破裂,向管道中写入数据的进程将收到内核传来的SIGPIPE信号(通常Broken pipe错误)
练习:父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,当输入quit结束。
提示:不需要加同步机制, 因为pipe无数据时读会阻塞。
先创建管道再fork,这样父子进程可以使用同一个无名管道。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
char buf[32];//定义一个数组
int fd[2];//定义两个文件描述符
if (pipe(fd) < 0)//创建管道并判断
{
perror("pipe err\n");
return -1;
}
printf("%d %d\n", fd[0], fd[1]);//打印一下文件描述符
int pid;
pid = fork();//创建父子进程
if (pid < 0)//创建失败
{
perror("fork err\n");
return -1;
}
else if (pid == 0)//子进程
{
while (1)
{
read(fd[0], buf, 32);//从读端读取管道中的数据到buf中
printf("%s\n", buf);//打印buf中的数据
}
}
else//父进程
{
while (1)
{
scanf("%s", buf);//从终端输入数据到buf中
if (strcmp(buf, "quit") == 0)//判断输入的是否为 quit,若是,则退出循环
{
break;
}
write(fd[1], buf, 32);//将 buf 中的数据从写端写到管道中
}
}
return 0;
}
3》有名管道
1> 特点
(1)有名管道可以使互不相关的两个进程互相通信。
(2)有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。但是读写数据不会存在文件中,而是在管道中。
(3)进程通过文件IO来操作有名管道。
(4)有名管道遵循先进先出规则
(5)不支持如lseek()操作
2> 函数接口
int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
mode:权限
返回值:成功:0
失败:-1,并设置errno号
注意对错误的处理方式:
如果错误是file exist时,注意加判断,如:if(errno == EEXIST)
注意:函数只是在路径下创建管道文件,往管道中写的数据依然是写在内核空间中
练习:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>//引用错误号头文件
int main(int argc, char const *argv[])
{
if (mkfifo("./fifo", 0777) < 0)
{
if (errno == EEXIST) //如果错误号信息是已存在则打印提示语句
printf("file exist!\n");
else
{
perror("mkfifo err");
return -1;
}
}
printf("mkfifo success\n");
return 0;
}
3> 注意事项
(1)只写方式打开会阻塞,一直到另一个进程把读端打开
(2)只写方式打开会阻塞,一直到另一个进程把写端打开
(3)可读可写,如果管道中没有数据,读会阻塞
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
char buf[32] = "";
if (mkfifo("./fifo", 0777) < 0)
{
if (errno == EEXIST) // 如果错误号信息是已存在则打印提示语句
printf("file exist!\n");
else
{
perror("mkfifo err");
return -1;
}
}
printf("mkfifo success\n");
// 打开文件
int fd = open("./fifo", O_RDONLY);//只读方式会阻塞
// int fd = open("./fifo", O_WRONLY);//只写方式会阻塞
// int fd = open("./fifo", O_RDWR);//可读可写方式不会阻塞
// 读写操作
write(fd, "hello", 5);//向管道写 hello
read(fd, buf, 32);//从管道中读取数据到buf中
printf("%s\n", buf);//打印buf中内容
return 0;
}
练习:通过两个进程实现 cp功能
一个进程读取源文件中的内容放到管道中,另一个进程读取管道中的内容写到目的文件中
读进程代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (mkfifo("./fifo", 0777) < 0) // 创建管道文件
{
if (errno == EEXIST)
printf("fifo exist\n");
else
{
perror("mkfifo err\n");
return -1;
}
}
printf("fifo success\n");
int fd, fd1;
char buf[32];
// 读
fd = open("fifo", O_WRONLY);//以只写方式打开管道文件
fd1 = open(argv[1], O_RDONLY);//以只读方式打开源文件
int n;//保存读取的数据个数
//循环读取源文件中的数据,先放到数组buf中,再从buf中读取放到管道中
while (n = read(fd1, buf, 32))
write(fd, buf, n);
//关闭文件描述符
close(fd);
close(fd1);
return 0;
}
写进程代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
// 重复执行创建管道操作不会出错,因为当官到文件第一次被创建后,后面在创建不会影响而是会打印 EEXIST 的错误,提示已经存在
if (mkfifo("./fifo", 0777) < 0) // 创建管道文件
{
if (errno == EEXIST)
printf("fifo exist\n");
else
{
perror("mkfifo err\n");
return -1;
}
}
printf("fifo success\n");
int fd, fd2;
char buf[32];
// 写
fd = open("fifo", O_RDONLY);//以只读方式打开管道文件
fd2 = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0777);//以只写方式打开目的文件
int n;
//循环读取管道中的数据,先放到数组buf中,再从buf中读取放到目的文件中
while (n = read(fd, buf, 32))
write(fd2, buf, n);
close(fd);
close(fd2);
return 0;
}
一起执行完毕
4> 有名管道和无名管道的区别
无名管道 | 有名管道 | |
使用场景 | 具有亲缘关系的进程间 | 不相干的进程间也可以使用 |
特点 | 半双工通信 固定的读端fd[0]和写端fd[1],看做一种特殊的文件可以通过文件操作 | 在文件系统中会存在管道文件,数据放在内核空间中,通过文件IO进行操作遵循先进先出,不支持lseek操作 |
函数 | pipe(),直接read/write | mkfifo(),先打开open,再读写read/write |
读写特性 | 当管道中无数据会度阻塞 当管道中写满时会写阻塞 关闭读端,往管道中写会管道破裂 | 只写方式下打开管道会阻塞,直到另一个进程把读端打开 只读方式下打开管道会阻塞,直到另一个进程把写端打开 可读可写方式下打开管道,如果管道中无数据会读阻塞 |
【2】信号
1》概念
(1)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
(2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件
(3)如果该进程当前并未处于执行态,则该信号就又内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
信号的生命周期
2》信号的响应方式
(1)忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP.
(2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数
(3)执行缺省操作:Linux对每种信号都规定了默认操作
3》信号种类
一些比较常用的信号:
SIGINT(2):中断信号,Ctrl-C 产生,用于中断进程
SIGQUIT(3):退出信号, Ctrl-\ 产生,用于退出进程并生成核心转储文件
SIGKILL(9):终止信号,用于强制终止进程。此信号不能被捕获或忽略。
SIGALRM(14):闹钟信号,当由 alarm() 函数设置的定时器超时时产生。
SIGTERM(15):终止信号,用于请求终止进程。此信号可以被捕获或忽略。termination
SIGCHLD(17):子进程状态改变信号,当子进程停止或终止时产生。
SIGCONT(18):继续执行信号,用于恢复先前停止的进程。
SIGSTOP(19):停止执行信号,用于强制停止进程。此信号不能被捕获或忽略。
SIGTSTP(20):键盘停止信号,通常由用户按下 Ctrl-Z 产生,用于请求停止进程。
4》 函数接口
1> 信号发送和挂起
#include <signal.h>
int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
sig:要发送的信号
返回值:成功 0
失败 -1
int raise(int sig);
功能:进程向自己发送信号
参数:sig:信号
返回值:成功 0
失败 -1
相当于:kill(getpid(), sig);
int pause(void);
功能:用于将调用进程挂起,直到收到被捕获处理的信号为止。
练习:
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//kill(getpid(), SIGKILL); //给进程发送信号,此例子是给当前进程发送SIGKILL信号
// raise(SIGKILL); //给当前进程发送SIGKILL信号,等同于kill(getpid(), SIGKILL);
// while (1);
pause(); //将进程挂起,作用类似死循环但是不占用CPU
return 0;
}
2> 定时器 alarm
man 2 alarm
unsigned int alarm(unsigned int seconds)
功能:在进程中设置一个定时器。当定时器指定的时间到了时,它就向进程发送SIGALARM信号。
参数:seconds:定时时间,单位为秒
返回值:如果调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
注意:一个进程只能有一个闹钟时间。如果在调用alarm时已设置过闹钟时间,则之前的闹钟时间被新值所代替。
常用操作:取消定时器alarm(0),返回旧闹钟余下秒数。
系统默认对SIGALRM(闹钟到点后内核发送的信号)信号的响应: 如果不对SIGALRM信号进行捕捉或采取措施,默认情况下,闹钟响铃时刻会退出进程。
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("%d\n", alarm(10)); //设定闹钟5秒后发送闹钟信号
sleep(1); //睡眠1秒,此时闹钟还剩9秒
printf("%d\n", alarm(3)); //打印剩余的9秒,设新闹钟3秒后发送闹钟信号
pause(); //为了不让进程结束,等待SIGALRM信号产生,产生之后结束当前进程
return 0;
}
3> 信号处理函数 signal()
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
handler:信号处理方式
SIG_IGN:忽略信号 (忽略 ignore)
SIG_DFL:执行默认操作 (默认 default)
handler:捕捉信号 (handler为函数名,可以自定义)
void handler(int sig){} //函数名可以自定义, 参数为要处理的信号
返回值:成功:设置之前的信号处理方式
失败:-1
补充:typedef给数据类型重命名
#include <stdio.h>
//给普通数据类型int重命名
typedef int size4;
//给指针类型int* 重命名
typedef int *int_p;
//给数组类型int [10]重命名
typedef int intArr10[10];
//给函数指针void (*)()重命名
typedef void (*fun_p)();
void fun()
{
printf("fun\n");
}
int main(int argc, char const *argv[])
{
size4 a = 10; //相当于int a=10;
int_p p = &a; //相当于int* p=&a;
intArr10 arr = {1, 2, 3}; //相当于int arr[10]={1,2,3};
fun_p fp = fun; //相当于 void (*fp)()=fun;
printf("%d\n", *p);
printf("%d\n", arr[0]);
fp();
return 0;
}
总而言之,定义变量的变量名写在哪里,用typedef给数据类型重命名的新名字就写在哪里。然后使用新名字定义变量的格式直接就可以为:新名字 变量名;
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig) //参数sig代表要处理的信号
{
if (sig == SIGINT)
printf("ctrl C: %d\n", sig);
else if (sig == SIGTSTP)
printf("ctrl Z: %d\n", sig);
}
int main(int argc, char const *argv[])
{
signal(SIGINT, SIG_IGN); //对SIGINIT信号设置忽略方式处理
//signal(SIGINT,SIG_DFL); //对SIGINIT信号设置缺省方式处理,也就是默认操作
signal(SIGINT, handler); //对SIGINIT信号设置捕捉方处理,也就是自定义处理方式
signal(SIGTSTP, handler);
//while (1); //为了让进程不要结束,因为等到信号真的来才能验证,不然进程就结束了
pause(); //收到被捕获处理的信号时会结束挂起
return 0;
}
【3】共享内存
1》特点
(1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
(2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程
将其映射到自己的私有地址空间。进程就可以直接读写这一内存区而不需要进行数据的拷贝,从而大大提高的效率。
(3)由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等
2》步骤
1> 创建key值
2> 创建或打开共享内存
3> 映射共享内存到用户空间
4> 撤销映射
5> 删除共享内存
3》 函数接口
key_t ftok(const char *pathname, int proj_id);
功能:创建出来的具有唯一映射关系的一个key值,帮助操作系统用来标识一块共享内存
参数:Pathname:已经存在的可访问文件的名字
Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
失败:-1
int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:key 键值
size 共享内存的大小
shmflg IPC_CREAT|IPC_EXCL|0777
返回值:成功 shmid
出错 -1
注意对错误的处理方式:
如果错误是file exist光打开共享内存不用设IPC_CREAT|IPC_EXCL了,加判断,如:if(errno == EEXIST)
void *shmat(int shmid,const void *shmaddr,int shmflg); //attaches
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:shmid 共享内存的id号
shmaddr 一般为NULL,表示由系统自动完成映射
如果不为NULL,那么有用户指定
shmflg:SHM_RDONLY就是对该共享内存只进行读操作
0 可读可写
返回值:成功:完成映射后的地址,
出错:-1(地址)
用法:if((p = (char *)shmat(shmid,NULL,0)) == (char *)-1)
int shmdt(const void *shmaddr); //detaches
功能:取消映射
参数:要取消的地址
返回值:成功0
失败的-1
int shmctl(int shmid,int cmd,struct shmid_ds *buf); //control
功能:(删除共享内存),对共享内存进行各种操作
参数:shmid 共享内存的id号
cmd
IPC_STAT 获得shmid属性信息,存放在第三参数
IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
IPC_RMID:删除共享内存,此时第三个参数为NULL即可
buf shmid所指向的共享内存的地址,空间被释放以后地址就赋值为null
返回值:成功0
失败-1
用法:shmctl(shmid,IPC_RMID,NULL);
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
int shmid;
key_t key;
char *p;
key = ftok("shm.c", 'a');
if (key < 0)
{
perror("key err");
return -1;
}
printf("key: %#x\n", key);
//打开或创建共享内存
shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0777); //如果共享内存不存在则创建,存在则返回-1
if (shmid <= 0)
{
if (errno == EEXIST) //如果共享内存已存在则,直接打开
shmid = shmget(key, 128, 0777); //直接打开已有的共享内存并且获得共享内存id
else
{
perror("shmget err");
return -1;
}
}
printf("shmid: %d\n", shmid);
//映射共享内存
p = (char *)shmat(shmid, NULL,0);
if(p == (char *)-1)
{
perror("shmat err");
return -1;
}
//操作共享内存
scanf("%s",p);
printf("%s\n", p);
//取消映射
shmdt(p);
//删除共享内存
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
进程间通信:
4》命令
ipcs -m :查看系统中的共享内存
ipcrm -m shmid:删除共享内存
ps: 可能不能直接删除掉还存在进程使用的共享内存。
这时候可以用ps -ef对进程进行查看,kill掉多余的进程后,再使用ipcs查看。
今天的分享就到这里结束啦,如果有哪里写的不好的地方,请指正。
如果觉得不错并且对你有帮助的话点个关注支持一下吧!