一,无名管道
1.Linux,常见的进程间通信方式:
**传统的进程间通信方式:****无名管道(pipe)、有名管道(fifo)和信号(signal)****System V IPC对象:**共享内存(shared memory)、消息队列(message queue)和信号量(semaphore)
**BSD:**套接字(socket)
2.无名管道通信
无名管道是内核空间实现的机制,只能用于亲缘进程间通信,无名管道的大小的64kb;

3.pipe函数
|-------|--------|------------------------|
| 所需头文件 | #include <unistd.h> ||
| 原型 | int pipe(int pipefd[2]); ||
| 功能 | 创建单向数据通道(半双工通信方式) 数据只能从读端pipefd[0]读取,从写端pipefd[1]写入,方向不可逆转; 数据并非在进程间进行传递,而是先写入内核缓冲区,再由读进程从内核缓冲区读取; ||
| 参数 | pipefd | 用于存放管道读写两端的文件描述符的数组首地址 |
| 返回值 | 成功:0 失败:-1;重置错误码 ||
代码示例:
cs
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc,const char *argv[])
{
//1.创建无名管道
//无名管道是单向的,支持半双工通信方式
//一端用于读(pipefd[0]),一端用于写(pipefd[1])
int fd[2] = {0};
int pipe_ret = 0;
pipe_ret = pipe(fd);
if(pipe_ret == -1)
{
perror("创建无名管道失败\n");
return -1;
}
//注意:1.pipe的文件描述符跟open函数的文件描述符使用的是同一组
//注意:2.fd[0]是读端,fd[1]是写端
printf("创建无名管道成功,fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);
//2.创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("创建子进程失败\n");
return -1;
}
else if(pid == 0)
{
//子进程
//子进程接收父进程的数据 读数据
//关闭写端
if(close(fd[1]) == -1)
{
perror("关闭子进程写端失败\n");
return -1;
}
printf("关闭子进程写端成功\n");
//从无名管道读取数据
char buf_str[BUFSIZ] = {0};
ssize_t read_ret = 0;
read_ret = read(fd[0],buf_str,BUFSIZ);
if(read_ret == -1)
{
perror("读取无名管道失败\n");
return -1;
}
printf("读取无名管道成功,数据:%s\n",buf_str);
//关闭读端
if(close(fd[0]) == -1)
{
perror("关闭子进程读端失败\n");
return -1;
}
printf("关闭子进程读端成功\n");
//子进程退出
exit(EXIT_SUCCESS);
}
else if(pid > 0)
{
//父进程
//父进程向子进程发送数据 写数据
//关闭读端
int close_ret = 0;
close_ret = close(fd[0]);
if(close_ret == -1)
{
perror("关闭父进程读端失败\n");
return -1;
}
printf("关闭父进程读端成功\n");
//往无名管道中写数据
char *str = "hello world";
ssize_t write_ret = 0;
write_ret = write(fd[1],str,strlen(str));
if(write_ret == -1)
{
perror("写入无名管道失败\n");
return -1;
}
printf("写入无名管道成功\n");
//关闭父进程写端
if(close(fd[1]) == -1)
{
perror("关闭父进程写端失败\n");
return -1;
}
printf("关闭父进程写端成功\n");
//等待子进程结束
wait(NULL);//不关注子进程的退出状态
printf("父进程等待子进程结束\n");
}
return 0;
}
4.无名管道(pipe)
**·无名管道特点:
·**无名管道依托具有亲缘关系的进程间的文件描述符继承特性来实现通信;
·数据在无名管道中只能单向传输,符合半双工的定义;
·在多数Linux系统中,常见无名管道的内核缓冲区默认大小为64KB;
·无名管道不能使用lseek函数,lseek用于随机访问文件位置,因此调用会出错返回-1;
**·无名管道读写特点:
·**若读端存在时,写管道满则阻塞;有4KB空间时唤醒,是linux内核为平衡效率与响应性的设计;
·若读端不存在时,写管道会破裂,会触发SIGPIPE信号;
·若写端存在时,读管道读取数据,没有数据的时候阻塞等待;
·若写端不存在时,读管道读取数据,没有数据的时候立即返回0(非阻塞);
//测试无名管道在Linux系统中的大小 64kb
cs
//测试无名管道在Linux系统中的大小 64kb
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc,const char *argv[])
{
//1.创建无名管道
int pipefd[2] = {0};
int pipe_ret = 0;
pipe_ret = pipe(pipefd);
if(pipe_ret == -1)
{
perror("创建无名管道失败\n");
return -1;
}
printf("创建无名管道成功\n");
//2.使用fork函数创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("创建子进程失败\n");
return -1;
}
else if(pid == 0)
{
//子进程
//关闭读端
close(pipefd[0]);
//往管道中进行写数据,专注测试一下无名管道的大小
//当把管道写满之后,管道就进入到一个阻塞状态
//直到管道有地方之后,才会继续往管道中进行写数据
//当管道被写满之后,并不会直接返回错误
char buf[1024] = {0};
ssize_t write_ret = 0;
int count = 0;
while(1)//循环写入数据,直到写满
{
write_ret = write(pipefd[1],buf,sizeof(buf));
if(write_ret == -1)
{
perror("写入无名管道失败\n");
return -1;
}
printf("写入无名管道成功\n");
count += write_ret;
printf("累计写入数据%d字节\n",count);
}
//关闭写端
close(pipefd[1]);
}
else if(pid > 0)
{
//父进程
//关闭写段
close(pipefd[1]);
wait(NULL);//等待子进程退出
//关闭读段
close(pipefd[0]);
}
return 0;
}
//3.测试无名管道的lseek函数,如果调用,就会失败,返回-1
cs
//3.测试无名管道的lseek函数,如果调用,就会失败,返回-1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc,const char *argv[])
{
//1.创建无名管道
//针对于单进程也可以创建无名管道
//但是针对于这样的操作是没有任何意义的
int fd[2] = {0};
if(pipe(fd) == -1)
{
perror("创建无名管道失败\n");
return -1;
}
printf("创建无名管道成功\n");
//关闭读端
if(close(fd[0]) == -1)
{
perror("关闭读端失败\n");
return -1;
}
printf("关闭读端成功\n");
//使用lseek函数
//第一个参数:文件描述符
//第二个参数:偏移量(0:不偏移。>0:向后偏移。<0:向后偏移)
//第三个参数:从哪里开始偏移 SEEK_SET:从文件开头开始偏移 SEEK_CUR:从当前位置开始偏移 SEEK_END:从文件末尾开始偏移
//返回值:成功返回偏移量,失败返回-1
//说明:在无名管道中使用lseek函数会报错
//错误信息为:使用lseek函数失败:Illegal seek(非法查找)
//无名管道必须要遵循先进先出的原则
if(lseek(fd[1],0,SEEK_SET) == -1)
{
perror("lseek失败\n");
return -1;
}
printf("lseek成功\n");
//关闭写端
if(close(fd[1]) == -1)
{
perror("关闭写端失败\n");
return -1;
}
printf("关闭写端成功\n");
return 0;
}
二,有名管道
1.有名管道通信
有名管道可以用于亲缘进程通信,也可以用于非亲缘进程通信,有名管道的大小是64KB;


2.mkfifo函数
|-------|----------|---------------------------------------------------|
| 所需头文件 | #include <sys/types.h> #include <sys/stat.h> ||
| 原型 | int mkfifo(const char *pathname,mode_t mode); ||
| 功能 | 创建有名管道 ||
| 参数 | pathname | 文件的路径和名字(只写名字则默认路径是当前路径) 如果管道文件存在,则会进行报错(提升文件已存在) |
| 参数 | mode | 文件的权限 最终权限=mode & ~umask |
| 返回值 | 成功: 0 失败:-1,重置错误码 ||
3.unlink函数
|-------|----------|--------------------------|
| 所需头文件 | #include <unistd.h> ||
| 原型 | int unlink(const char *pathname); ||
| 功能 | 删除文件系统中的"文件名链接" ||
| 参数 | pathname | 文件的路径和名字(只写名字则默认路径是当前路径) |
| 返回值 | 成功: 0 失败:-1,重置错误码 ||
4.有名管道(FIFO)
有名管道特点:
· 数据在有名管道中只能单向传输,若要双向通信,需创建两个管道,符合半双工的定义;
· 在多数Linux系统中,常见有名管道的内核缓冲区默认大小为64KB;
· 有名管道不能使用lseek函数,lseek用于随机访问文件位置,因此调用会出错返回-1;
有名管道写操作特点:
·当读端已打开时:
· 写入数据时会尽可能写入(受管道缓冲区大小限制);
· 若管道已满,写操作会阻塞直到有空间;
· 若写操作在阻塞期间读端关闭,会产生SIGPIPE信号(管道破裂);
·当读端未打开时:
.写进程会在open()调用时会阻塞,直到有读进程打开该FIFO;
· 若写进程以非阻塞方式打开FIFO且无读端,open()会返回错误;
有名管道读操作特点:
· 当写端已打开时:
· 读取数据时,有多少读取多少;
· 若管道为空,读操作会阻塞等待数据写入;
·当写端未打开时:
· 读进程在open()调用会阻塞,直到有写进程打开该管道;
· 若写端曾经打开但已关闭,读操作会读取剩下数据,读完后返回0(不会阻塞);
· 若读进程以非阻塞方式打开FIFO且无写端,open会返回成功;
有亲缘关系代码示例:
cs
//有名管道在使用的时候必须要保证读端和写端都被打开,open操作才会返回
//有名管道一般不用于亲缘关系进程之间的通信
//有名管道一般是用于非亲缘之间的通信
//有名管道中必须要调用unlink函数清理管道文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(int argc,const char *argv[])
{
//1.对输入参数的参数个数进程检查
if(argc != 2)
{
printf("输入的参数个数错误\n");
printf("例如:./a.out filename\n");
return -1;
}
//2.创建有名管道 mkfifo函数
//如果管道文件存在,则会报错
if(mkfifo(argv[1],0664) == -1)
{
perror("创建有名管道失败");
return -1;
}
printf("创建有名管道成功\n");
//3.创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("创建子进程失败\n");
return -1;
}
else if(pid == 0)
{
//子进程
//打开管道文件
//在子进程中以只写的方式打开管道文件
//内核管道缓冲区是在第一次open函数调用(打开管道文件)的时候才会进行分配
int fd = open(argv[1],O_WRONLY);
if(fd == -1)
{
perror("子进程打开有名管道失败");
//清理有名管道
unlink(argv[1]);
return -1;
}
printf("子进程打开有名管道成功\n");
//往管道文件写入数据
char buf[] = "hello world\n";
ssize_t ret_write = 0;
ret_write = write(fd,buf,sizeof(buf));
if(ret_write == -1)
{
perror("子进程写入有名管道失败");
return -1;
}
printf("子进程写入有名管道成功\n");
//关闭管道文件
if(close(fd) == -1)
{
perror("子进程关闭有名管道失败");
return -1;
}
printf("子进程关闭有名管道成功\n");
//让子进程退出
exit(EXIT_SUCCESS);
}
else if(pid > 0)
{
//父进程
//等待子进程退出
//打开管道文件
//在父进程中以只读的方式打开管道文件
//内核管道缓冲区是在第一次open函数调用(打开管道文件)的时候才会进行分配
int fd = open(argv[1],O_RDONLY);
if(fd == -1)
{
perror("父进程打开有名管道失败");
return -1;
}
printf("父进程打开有名管道成功\n");
wait(NULL);//不关注子进程的退出状态值
//从管道文件读取数据
char buf[1024] = {0};
ssize_t ret_read = 0;
ret_read = read(fd,buf,sizeof(buf));
if(ret_read == -1)
{
perror("父进程读取有名管道失败");
//清理有名管道
unlink(argv[1]);
return -1;
}
printf("父进程读取有名管道成功\n");
//针对于unlink函数仅仅是删除文件系统中的"文件名链接"
//不会影响已经打开的文件描述符
//已经打开的文件描述符还可以正确的从管道文件中进行读写操作
//可以避免误删文件名或者是提前删除文件名导致对管道文件中的数据读写操作失败
printf("读取到的数据为:%s",buf);
//关闭管道文件
if(close(fd) == -1)
{
perror("父进程关闭有名管道失败");
return -1;
}
printf("父进程关闭有名管道成功\n");
//自动清理有名管道
if(unlink(argv[1]) == -1)
{
perror("自动清理有名管道失败");
return -1;
}
printf("自动清理有名管道成功\n");
//不要重复使用unlink函数
}
return 0;
}
无亲缘关系示例代码:
读
cs
//这个文件用于读取有名管道中的数据
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,const char *argv[])
{
//1.对输入参数的参数个数进行检查
if(argc != 2)
{
printf("输入的参数个数有误\n");
printf("例如:./a.out pathname\n");
return -1;
}
//2.打开管道文件
int fd = open(argv[1],O_RDONLY);
if(fd == -1)
{
perror("读段:打开管道文件失败\n");
return -1;
}
printf("读段:打开管道文件成功\n");
//3.从管道文件中读数据
ssize_t read_ret = 0;
char buf[BUFSIZ] = {0};
read_ret = read(fd,buf,sizeof(buf));
if(read_ret == -1)
{
perror("读段:读取管道文件失败\n");
return -1;
}
printf("读段:读取管道文件成功\n");
printf("读取到的数据为:%s\n",buf);
//4.关闭管道文件
if(close(fd) == -1)
{
perror("读段:关闭管道文件失败\n");
return -1;
}
printf("读段:关闭管道文件成功\n");
//5.使用unlink函数删除管道文件
//针对于同一管道文件,只需要使用一次unlink函数即可
if(unlink(argv[1]) == -1)
{
perror("读段:删除管道文件失败\n");
return -1;
}
printf("读段:删除管道文件成功\n");
return 0;
}
写
cs
//这个文件用于写入有名管道中的数据
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,const char *argv[])
{
//1.对输入参数的参数个数进行检查
if(argc != 2)
{
printf("输入的参数个数有误\n");
printf("例如:./a.out pathname\n");
return -1;
}
//创建管道文件 mkfifo函数 也可以使用mkfifo命令进行创建管道文件
//针对于创建管道文件是写在读端还是写在写端,要根据实际情况来定
int mkfifo_ret = 0;
mkfifo_ret = mkfifo(argv[1],0664);
if(mkfifo_ret == -1)
{
perror("创建有名管道失败");
return -1;
}
printf("创建有名管道成功\n");
//2.打开管道文件
int fd = open(argv[1],O_WRONLY );
if(fd == -1)
{
perror("写段:打开管道文件失败\n");
return -1;
}
printf("写段:打开管道文件成功\n");
//3.向管道文件中写数据
ssize_t write_ret = 0;
char buf[] = "123456";
write_ret = write(fd,buf,sizeof(buf));
if(write_ret == -1)
{
perror("写段:写入管道文件失败\n");
return -1;
}
printf("写段:写入管道文件成功\n");
//4.关闭管道文件
if(close(fd) == -1)
{
perror("写段:关闭管道文件失败\n");
return -1;
}
printf("写段:关闭管道文件成功\n");
//5.使用unlink函数删除管道文件(在写操作中不进行删除管道文件)
//一般同一个管道文件只删除一次
//一般是在读操作中进程删除管道文件,因为读操作是最后从管道文件中1进行读取数据
//再次进行关闭管道文件
return 0;
}
三,信号
信号是中断的一种模拟,它是基于Linux内核实现的;
用户可以给进程发送信号,进程可以给进程发送信号,Linux内核也可以给进程发信号;
对信号处理的三种方式:默认,忽略,捕捉;

常用信号一
|-------------|-------------------------------------------|----------|
| 信号名 | 含义 | 默认操作 |
| SIGHUP | 用户终端关闭时产生,发给与该终端关联会话内的所有进程 | 终止 |
| SIGINT | 用户键入INTR字符(Ctrl+C)时产生,内核发送到当前终端所有前台进程 | 终止 |
| SIGQUIT | 由QUIT字符(通常是Ctrl+\)产生,与SIGINT类似 | 终止 |
| SIGILL | 进程企图执行非法指令时产生 | 终止 |
| SIGSEGV | 非法访问内存时产生,如野指针,缓冲区溢出 | 终止 |
| SIGPIPE | 进程往没有读端的管道中写入时产生,代表"管道破裂" | 终止 |
常用信号二
|---------------|----------------------------------------|----------|
| 信号名 | 含义 | 默认操作 |
| SIGKILL | 用来结束进程,且不能被捕捉和忽略 | 终止 |
| SIGSTOP | 用来暂停进程,且不能被捕捉和忽略 | 暂停进程 |
| SIGTSTP | 用于暂停进程,用户可键入SUSP字符(通常是Ctrl+Z)发出此信号 | 暂停进程 |
| SIGCONT | 让进程进入运行态 | 继续运行 |
| SIGALRM | 通知进程定时器时间已到 | 终止 |
| SIGUSR1/2 | 保留给用户程序使用 | 终止 |
| SIGCHLD | 当子进程退出时,给父进程发送该信号 | 忽略 |
signal函数
|-----------|-------------|--------------------------------------------------------|
| 所需头文件 | #include <signal.h> ||
| 原型 | typedef void ("sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); ||
| 功能 | 建立信号与处理方式的关联 ||
| 参数 | signum | 信号的编号 |
| 参数 | handler | 处理方式 SIG_IGN :忽略 SIG_DFL :默认 传递函数地址:捕捉 |
| 返回值 | 成功:返回handler首地址 失败:返回SIG_ERR,重置错误码 ||
示例代码:
cs
//使用signal函数建立信号与处理方式的关联
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
//信号处理方式的函数
void my_func(int sig)
{
printf("捕捉到SIGINT信号\n");
}
int main(int argc,const char *argv[])
{
//第一个参数是用来指定信号的编号
//第二个参数是用来指定信号的处理方式 忽略
// if(SIG_ERR == signal(SIGINT,SIG_IGN))
// {
// perror("建立信号与处理方式关联失败");
// return -1;
// }
// printf("建立信号与处理方式关联成功\n");
//指定信号的处理方式为默认处理方式
// if(SIG_ERR == signal(SIGINT,SIG_DFL))
// {
// perror("建立信号与处理方式关联失败");
// return -1;
// }
// printf("建立信号与处理方式关联成功\n");
//指定信号处理方式为捕捉方式
if(SIG_ERR == signal(SIGINT,my_func))
{
perror("建立信号与处理方式关联失败");
return -1;
}
printf("建立信号与处理方式关联成功\n");
while(1)
{
//每隔一秒打印一次hello world
printf("hello world\n");
sleep(1);
}
return 0;
}
raise函数
|-----------|---------|---------------|
| 所需头文件 | #include <signal.h> ||
| 原型 | int raise(int sig); ||
| 功能 | 会向自身发送指定的信号 ||
| 参数 | sig | 要发送的信号的编号 |
| 返回值 | 成功:返回值0; 失败:返回非0; ||
示例代码:
cs
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
void handler(int sig)
{
//SIGCHLD信号捕捉之后的操作
if(sig == SIGCHLD)
{
printf("子进程退出,捕捉到了SIGCHLD信号\n");
//父进程回收子进程的资源
wait(NULL); //不关注子进程的退出状态值
}
}
int main(int argc,const char *argv[])
{
//1.建立信号与信号处理方式的关联 SIGCHLD
//SIGCHLD:是当子进程退出的时候,内核会向父进程发送该信号
//捕捉SIGCHLD信号
if(SIG_ERR == signal(SIGCHLD,handler))
{
perror("建立信号与信号处理方式关联失败");
return -1;
}
printf("建立信号与信号处理方式关联成功\n");
//2.创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("创建子进程失败");
return -1;
}
else if(pid == 0)
{
//子进程
printf("我是子进程,我的pid=%d\n",getpid());
printf("子进程延时5秒之后退出\n");
sleep(5);
exit(EXIT_SUCCESS); //子进程正常退出
}
else if(pid > 0)
{
//父进程
printf("我是父进程,我的pid=%d\n",getpid());
printf("父进程延时8秒之后退出\n");
sleep(8);
//父进程向自身发送指定的信号
//SIGKILL:杀死进程.不能被捕捉和忽略
if(0 != raise(SIGKILL))
{
printf("父进程向自身发送指定的信号失败");
}
//printf("父进程向自身发送指定的信号成功\n");
}
return 0;
}
kill函数
|-----------|---------|------------------------------------------------------------------------------------------------------------------------|
| 所需头文件 | #include <signal.h> #include <sys/types.h> ||
| 原型 | int kill(pid_t pid,int sig); ||
| 功能 | 给任意进程发信号 ||
| 参数 | sig | 信号的编号 |
| 参数 | pid | 发送信号目标 >0 向pid对应的单个进程发送信号(最常用场景) 0 向同进程组的所有进程发送信号 -1 向所有有权限操作的进程发送信号(init进程除外,避免影响系统核心进程) <-1 给进程组ID等于-pid的进程发信号 |
| 返回值 | 成功:返回值0; 失败:-1,重置错误码; ||
示例代码:
cs
//模拟kill命令:kill -信号编号 进程号
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc,const char *argv[])
{
//1.对输入参数的参数个数进行检查
//使用方式:./3-kill -信号编号 进程号
if(argc != 3 || argv[1][0] != '-')
{
printf("输入的参数个数有误\n");
printf("usage:./3-kill -信号编号 进程号\n");
return -1;
}
//2.将参数转换为整数(将字符串转换为整数) atoi函数
int signo = atoi(argv[1]+1);
//信号一共有64个,是从1信号开始的
if(signo < 1 || signo > 64)
{
printf("输入的信号编号有误\n");
return -1;
}
int pid = atoi(argv[2]);
if(pid <= 0 )
{
printf("输入的进程号有误\n");
return -1;
}
//3.使用kill函数发送信号
//第一个参数要发送信号的目标
//第二个参数要发送的信号的编号
if(-1 == (kill(pid,signo)))
{
perror("使用kill函数发送信号失败");
return -1;
}
printf("使用kill函数发送信号成功\n");
//4.打印结果
printf("kill -%d %d\n",signo,pid);
return 0;
}
atoi函数
|-----------|----------|-----------------|
| 所需头文件 | #include <stdlib.h> ||
| 原型 | int atoi(const char *nptr); ||
| 功能 | 将字符串转换为整数 ||
| 参数 | nptr | 要转换的字符串的首地址 |
| 返回值 | 成功:返回转换后的整数; 失败:返回0;如果转换后整数超出int范围,那么行为未定义; ||