目录
1、管道通信
管道通信是一个设备中进程与进程之间通信的一种方式,分为无名管道和有名管道两种。前者只能用于有亲缘关系的进程之间的通信,如父子进程等,后者则没有亲缘限制。此外,管道通信从根本上还是通过内核的一种半双工的通信方式。
1.1、无名管道
无名管道又称匿名管道,即通信双方无需知道对方的pid号,仅通过一些管道提供的描述符便可往管道缓冲区 读写数据。在具体使用上,管道应比子进程更先被创建,因为而后被创建的子进程会拷贝父进程的地址空间,从而保证父子进程使用的是同一条管道。当然,管道通信并不限制进程数量。
pipe( )是创建无名管道的一个重要函数,我们可以查看其函数说明:
bash
man 2 pipe
- int pipefd[2]:该数组是用于存放从pipe()取出的管道描述符。其中,pipefd[0]是读取数据的描述符,pipefd[1]是写入数据的描述符。
- int flags:不常用,不说。
- 返回值:成功返回0,反之返回-1。
再看个例子便知道怎么用了:
该例子完成的任务:父进程从终端中读取用户输入的字符并写入管道,然后子进程从管道中读取数据之后将字符一个个打印到终端中来。
注意的点:管道读取是一种阻塞式读取,即如果某一进程想从本没有数据的管道中读取数据,就会一直阻塞在那里,等到有数据进入管道缓冲区。并且这种阻塞只发生在最开始的时候,如果中途管道没有数据了,它还是可以正常读取,只不过返回值是0.
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int pipe_fd[2];
char buf;
if(argc != 2)
{
printf("Usage :%s <string>\n", argv[0]);
return -1
}
pipe(pipe_fd); //create a pipe
pid_t cpid = fork(); //create a child process
if(cpid == 0) //enter the child process
{
close(pipe_fd[1]);
while(read(pipe_fd[0], &buf, 1))
write(1, &buf, 1);
printf("\n");
close(pipe_fd[0]);
_exit(0); //recommend to use the _exit() to exit in the child process
}
else //enter the parent process
{
close(pipe_fd[0]);
write(pipe_fd[1], argv[1], strlen(argv[1]));
close(pipe_fd[1]);
wait(NULL); //waiting for his child to exit
exit(0);
}
return 0;
}
1.1、有名管道
有名管道相较于无名管道,可以用于无亲缘关系进程之间的通信。并且它的逻辑也更为清晰,使用的时候,我们可以在文件夹中创造一个管道文件,然后其他进程便可往这个管道文件读写数据,最终实现进程通信的目的。
下面看看具体操作步骤:
第一步,在文件夹中创建一个管道文件(以test_pipe为例)。
bash
mkfifo test_pipe
除了在终端中创建一个FIFO文件夹,我们还可以在程序中创建文件夹。同样是用mkfifo函数,参考
bash
man 3 mkfifo
第二步,创建一个进程往test_pipe写入数据。
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("./test_pipe", O_WRONLY); //open the pipe file
write(fd, "hello", 5); //write something into the file
close(fd); //close the file, don't forget!
return 0;
}
第三步,创建一个新的进程往test_pipe写入数据。
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("./test_pipe", O_WRONLY);
if(fd == -1)
{
printf("an error occured opening the pipe file\n");
return -1;
}
while(1)
{
write(fd, "hello!", 6);
printf("writing successfully!\n");
sleep(1);
}
close(fd);
return 0;
}
第四步,创建一个新的进程从test_pipe读出数据。注意在读取操作的时候要考虑读取延时问题,y因为如果读的速度比写的速度还慢,那可能会读到的不仅仅是这一次写入的数据,还可以能拼接一些上一次的数据。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd = open("./test_pipe", O_RDONLY);
char buf[6];
while(1)
{
//sleep(1);
read(fd, buf, 6);
printf("what i read is: %s\n", buf);
memset(buf, 0 ,sizeof(buf)); //clear the buf
}
close(fd);
return 0;
}
通过以上步骤便可以实现两个没有亲缘关系之间的通信了。但在我学习过程中,还遇到很多问题,这也一一列举出来吧。
第一 ,有名管道的通信是读写阻塞的,但它阻塞不是阻塞在read和write函数上,而是阻塞在open函数上。这似乎意味着,有名管道的通信始于两个进程同时访问该管道。
第二,当写进程终止之后,读进程还是可以继续运行,只是读出的数据是"空"而已。但如果读进程终止,写进程也会跟着终止。个人感觉问题出现在write函数上,即write函数检测到没有进程读数据,它就会停止,我写了程序验证了一下也大抵如此。
2、信号处理
2.1、信号的种类和发送
信号的发送最常用的是kill()函数,kill既可以在终端直接使用,也可以在代码里以函数的形式使用。我们先看看发送的信号的种类吧。
总共有64种信号,一般常用的就几个,其他需要的时候再去网上查就行了。如果我们想在终端中使用命令kill某一个进程,可以按以下操作来。
bash
kill -<number> <pid>
比如我这里kill了一个叫test的进程。
接下来看看kill怎么在程序中使用,在此之前可以看一下它的官方文档(man 2 kill),在这里我就直接提取它的函数原型出来。
c
int kill(pid_t pid, int sig);
pid_t pid:指目标进程的pid号,其实就是你要把信号发给哪个进程。
int sig:你要发的是几号信号。
返回值:成功返回0,失败返回-1。
在函数中的使用方法也十分简单,下面以杀死pid号为3234的进程为例。
c
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
int main()
{
//if you wanna kill yourself, you can use kill(getpid(), 9);
int flag = kill(3234, 9);
if(flag < 0){
printf("kill error!");
}
return 0;
}
除了kill()函数可以用来发送信号,alarm(), raise()等函数也可以。
其中:
c
raise(sig); <==> kill(getpid(), sig);
而alarm函数的原型是:
c
unsigned int alarm(unsigned int seconds);
使用方法也很简单,该函数的输入参数是N秒。当一个进程调用这个函数N秒之后,该进程就会收一个SIGALAM的信号,该信号可以终止进程。
2.2、信号的接受和处理
进程信号接受的根本要求肯定是该进程还存在,一般而言保证进程存在有三种方式:
- 采用sleep()函数让进程进入睡眠,但睡眠状态并不是永远的,它有一个输入参数来限制睡眠的时间。比如睡眠三秒:sleep(3);
- 采用pause()函数让进程进入睡眠状态,进入睡眠状态之后,无论你对进程进行任何操作,进程都会退出睡眠状态。
- 利用while循环让进程一直处于运行状态。
在进程存在的前提下便可以考虑进程对于信号的处理方式了,处理方式也是有三种,分别是忽略(SIG_IGN),默认(SIG-DFL)和捕获。而信号的处理一般用signal()函数,下面我们来看看它的函数原型:
c
sighandler_t signal(int signum, sighandler_t handler);
int signum:处理的信号的编码/种类,除了该信号,其他信号一概忽略。
sighandler_t handler:当接受到该信号的时候,处理的方式是什么(默认、忽略还是捕获)。
返回值:成功则返回被处理信号的编号,失败则返回SIG_ERR。
看个例子吧。
c
#include <stdio.h>
#include <signal.h>
void t_function(int sig);
int main()
{
signal(SIGINT, t_function);
while(1); //keep this process running.
return 0;
}
void t_function(int sig)
{
printf("capture successfully!\n");
}
将以上代码写进一个叫test.c的文件,然后再编译成名为a.out的可执行文件。接着执行a.out,并在另一个终端输入kill -2 5961给a.out这个进程发送SIGINT的信号,可以看到捕获成功。
这里有几个注意的点:
第一,SIGKILL和SIGSTOP不能被捕获和忽略。
第二,捕获的函数,必须有且一个属于参数,这个输入参数其实是signal的标号,比如SIGINT的标号就是2。
第三,SIGINT其实就是键入的Ctrl+C的组合效果。(在Linux系统中Ctrl+C表示中断)