/**
* @author IYATT-yx
* @brief 验证进程之间无法使用全局变量进行通信
*/
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int num = 15;
int main(void)
{
pid_t pid = fork();
if (pid > 0)
{
printf("父: %d\n", num);
++num;
printf("父: %d\n", num);
}
else if (pid == 0)
{
printf("子: %d\n", num);
--num;
printf("子: %d\n", num);
}
else
{
perror("fork");
}
}
运行结果:
/**
* @author IYATT-yx
* @brief 进程间使用文件进行通信
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
int main(void)
{
int fdRW = open("/tmp/temp", O_CREAT | O_RDWR, 0664);
if (fdRW == -1)
{
perror("open /tmp/temp");
return -1;
}
// 子进程的虚拟地址空间和父进程基本一样 (除pid)
// 可以使用同一个文件描述符,且都指向相同的文件
pid_t pid = fork();
if (pid == 0)
{
const char *str = "验证使用文件在进程间通信";
write(fdRW, str, strlen(str));
close(fdRW);
}
else if (pid > 0)
{
wait(NULL);
char buf[1024];
memset(buf, 0, 1024);
lseek(fdRW, 0, SEEK_SET);
read(fdRW, buf, sizeof(buf));
printf("%s\n", buf);
remove("/tmp/temp");
}
}
IPC常用4种方式
- 管道 (简单)
- 内存映射 (通信的进程之间可以没有"血缘"关系)
- 信号 (麻烦,但是信号属于操作系统的功能, 资源开销小)
- 本地套接字 (稳定, 见后面网络编程部分)
pipe(匿名管道) | |
---|---|
本质 | ①内核缓冲区 ②不占用磁盘空间,伪文件,但是可以当作文件读写 |
特点 | ①两部分: 读端和写端各对应一个文件描述符,写端流入,读端流出 ②操作管道的进程被 销毁后,自动销毁管道 ③管道默认阻塞 |
原理 | 内部实现方式: 队列 * 环形队列 * 先进先出 缓冲区大小: * 默认4K * 大小根据实际情况适当调整 |
局限性 | 队列: * 数据只能读取一次 半双工: * 数据可以在一个信号载体的两个方向上传输,但是不能同时传输. 匿名管道: * 有"血缘"关系的进程之间通信 |
/**
* @author IYATT-yx
* @brief 借助管道实现 ps ajx | grep bash , 父子进程通信
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(void)
{
// 创建管道
// 下标0为读,下标1为写
int fdPipe[2];
if (pipe(fdPipe) == -1)
{
perror("pipe");
return -1;
}
pid_t pid = fork();
// 子进程执行 ps ajx
// 执行结果默认是写到 STDOUT_FILENO
// 此处需要重定向到 fdPipe[1], 写入管道
if (pid == 0)
{
// 写时,关读
close(fdPipe[0]);
// 重定向文件描述符,写到终端改为写到管道
dup2(fdPipe[1], STDOUT_FILENO);
execlp("ps", "", "ajx", NULL);
perror("execlp ps");
return -1;
}
// 父进程执行 grep bash
// 获取查找来源默认是从 STDIN_FILENO
// 此处需要重定向到 fdPipe[0], 从管道读取
else if (pid > 0)
{
// 读时,关写
close(fdPipe[1]);
// 重定向文件描述符,从终端读取改为从管道读取
dup2(fdPipe[0], STDIN_FILENO);
execlp("grep", "", "--color=auto", "bash", NULL);
perror("execlp grep");
return -1;
}
}
/**
* @author IYATT-yx
* @brief 借助管道实现 ps ajx | grep bash, 兄弟进程通信
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
// 创建管道
// 下标0为读,下标1为写
int fdPipe[2];
if (pipe(fdPipe) == -1)
{
perror("pipe");
return -1;
}
int i;
for (i = 0; i < 2; ++i)
{
pid_t pid = fork();
if (pid == -1)
{
perror("fork");
return -1;
}
// 防止子进程继续创建子进程
else if (pid == 0)
{
break;
}
}
// 子进程1: ps ajx
if (i == 0)
{
close(fdPipe[0]);
dup2(fdPipe[1], STDOUT_FILENO);
execlp("ps", "", "ajx", NULL);
perror("execlp ps");
return -1;
}
// 子进程2: grep bash
else if (i == 1)
{
close(fdPipe[1]);
dup2(fdPipe[0], STDIN_FILENO);
execlp("grep", "", "--color=auto", "bash", NULL);
perror("execlp grep");
return -1;
}
// 父进程: 等待回收子进程
else if (i == 2)
{
close(fdPipe[0]);
close(fdPipe[1]);
while (wait(NULL) != -1)
{
;
}
}
}
管道的读写 | |
---|---|
读 | 有数据: * read()正常读取,返回读取的字节数 无数据: * 写端全部关闭: * * * * read()解除阻塞,返回0,相当于读取到文件的尾部 * 没有全部关闭: * * * * read()阻塞 |
写 | 读端全部被关闭: * 管道破裂,进程被终止,内核给当前进程发 SIGPIPE 信号 读端没全部关闭: * 缓冲区写满了,write阻塞 * 缓冲区没写满,write继续写 |
# 查看pipe大小
ulimit -a
512bytes x 8 = 4096byte = 4KB
也可以借助fpathconf
函数查看各种属性信息,具体可使用man
命令获取更多帮助.
/**
* @author IYATT-yx
* @brief 借助 fpathconf 查看 pipe 缓冲区大小
*/
#include <stdio.h>
#include <unistd.h>
int main(void)
{
// 创建管道
// 下标0为读,下标1为写
int fdPipe[2];
if (pipe(fdPipe) == -1)
{
perror("pipe");
return -1;
}
long size = fpathconf(fdPipe[0], _PC_PIPE_BUF);
size >>= 10;
printf("pipe缓冲区大小: %ldKB\n", size);
}
pipe默认读写都是阻塞,可以使用前面文件属性
部分提到的fcntl
函数修改为非阻塞.
// fd 为管道
// 设置读端为非阻塞,写端同理
// 获取文件描述符原open时的属性
int flag = fcntl(fd[0], F_GETFL);
// 添加非阻塞属性
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flag);
fifo (有名管道) | |
---|---|
特点 | 在磁盘上存在一个文件,大小为0,伪文件,实际数据依然在内核中. 半双工. |
使用场景 | 没有"血缘"关系的进程之间通信 |
创建方式 | 命令: mkfifo 函数: mkfifo() |
fifo使用和读写普通文件几乎一样,先open(),然后read()和write(),只是不能用open来创建,可以用mkfifo函数或命令来创建.
mmap (内存映射)
- 作用: 将磁盘文件的数据映射到内存,用户通过修改内存来修改磁盘文件,借助mmap也可以实现"非血缘"关系的进程间的通信.
// 创建内存映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数 | 说明 |
---|---|
addr | 映射区的首地址, 传 NULL |
length | 映射区的大小 一般指定为文件的大小,但是内部实际是:文件不足4K为4K,超过4K为可以容纳文件大小的最小的4K的整数倍 |
prot | 映射区权限: PROT_READ 读 PROT_WRITE 写 使用时至少要有读权限 |
flags | 标志位参数: MAP_SHARED 修改内存数据会同步到磁盘 MAP_PRIVATE 修改了内存数据不会同步到磁盘 MAP_ANONYMOUS 或 MAP_ANON 匿名 |
fd | 要映射的文件对应的文件描述符 |
offset | 映射文件的偏移量 * 偏移量必需是4K的整数倍* * 一般不偏移设为 0 |
返回值 | 成功: 映射区的首地址 失败: 返回 MAP_FAILED |
// 释放内存映射区
int munmap(void *addr, size_t length);
参数 | 说明 |
---|---|
addr | mmap的返回值, 映射区的首地址 |
length | mmap的参数length, 映射区的长度 |
/**
* @author IYATT-yx
* @brief 使用内存映射读取磁盘文件 /etc/apt/sources.list
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main(void)
{
int fdR = open("/etc/apt/sources.list", O_RDONLY);
if (fdR == -1)
{
perror("open");
return -1;
}
// 获取文件内容长度
size_t len = (size_t)lseek(fdR,0, SEEK_END);
// 创建内存映射区
void *ptr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fdR, 0);
// 显示内存映射区的内容
printf("%s\n", (char *)ptr);
// 释放内存映射区
if (munmap(ptr, len) == -1)
{
perror("munmap");
}
}
/**
* @author IYATT-yx
* @brief 使用内存映射进行进程间通信 (有名)
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/wait.h>
int main(void)
{
int fd = open("/tmp/mmapTemp", O_RDWR | O_CREAT, 0664);
if (fd == -1)
{
perror("open mmapTemp");
return -1;
}
ftruncate(fd, 4096);
size_t len = (size_t)lseek(fd, 0, SEEK_END);
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
return -1;
}
pid_t pid = fork();
if (pid == 0)
{
strcpy(ptr, "测试mmap进程间通信");
}
else if (pid > 0)
{
wait(NULL);
printf("%s\n", (char *)ptr);
}
munmap(ptr, len);
close(fd);
}
/**
* @author IYATT-yx
* @brief 使用内存映射进行进程间通信 (匿名), 不借助文件
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(void)
{
size_t len = 4096;
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
return -1;
}
pid_t pid = fork();
if (pid == 0)
{
strcpy(ptr, "测试mmap进程间通信");
}
else if (pid > 0)
{
wait(NULL);
printf("%s\n", (char *)ptr);
}
munmap(ptr, len);
}
在"非血缘"关系的进程间通信时,不能使用匿名映射区. 需要创建文件, 分别进行内存映射操作.
信号
特点 | 简单 携带信息量少 使用在某个特定的场景中 |
信号的状态 | 产生 未决: 没有被处理的 递达: 信号被处理了 |
产生 | 键盘: Ctrl + C 终止进程 命令: kill 系统函数: kill() 软条件: 定时器 硬件: 段错误, 除0错误 |
信号的优先级高,进程收到信号后,会暂停正在执行的任务,处理完信号后,再继续原来的工作
查询信号相关的文档man 7 signal
, 查看信号宏kill -l
// 向指定pid的进程发送信号
int kill(pid_t pid, int sig);
// 向进程自己发送信号
int raise(int sig);
// 向自己发送 SIGABRT 信号 (异常终止)
void abort(void);
/**
* @author IYATT-yx
* @brief raise 向自己发信号
*/
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid = fork();
if (pid > 0)
{
int sig;
wait(&sig);
if (WIFSIGNALED(sig))
{
printf("子进程被信号 %d 终止\n", WTERMSIG(sig));
}
}
else if (pid == 0)
{
raise(SIGINT);
}
}
// 定时发送信号 SIGALRM
unsigned int alarm(unsigned int seconds);
/**
* @author IYATT-yx
* @brief alarm 定时发送信号终止进程
*/
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
int main(void)
{
alarm(6);
sleep(2);
// 重新设定定时器
printf("上一个定时器还剩 %d 秒\n", alarm(3));
while (true)
{
printf("循环\n");
sleep(1);
}
}
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
struct itimerval
{
// 周期
struct timeval it_interval;
// 首次
struct timeval it_value;
};
struct timeval
{
// 秒
time_t tv_sec;
// 微秒
suseconds_t tv_usec;
};
代码应用示例可见下一节 7 Linux 守护进程
信号集
未决信号集:
- 没有被当前进程处理的信号
- 收到的信号首先放进未决信号集,然后判断阻塞信号集中该信号对应的标志位是否为 1 , 如果为 1 则不处理,如果为 0 则处理.
阻塞信号集:
- 将某个信号放到阻塞信号集,则这个信号就不会被进程处理
- 阻塞解除后,信号被处理
// 将set集合置空
int sigemptyset(sigset_t *set);
// 将所有信号加入set集合
int sigfillset(sigset_t *set);
// 将signo信号加入到set集合
int sigaddset(sigset_t *set, int signum);
// 从set集合中移除signo信号
int sigdelset(sigset_t *set, int signum);
// 判断信号是否存在
int sigismember(const sigset_t *set, int signum);
// 屏蔽and接触信号,将自定义信号集设置给阻塞信号集
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// 内核将未决信号集写入set
int sigpending(sigset_t *set);
/**
* @author IYATT-yx
* @brief 设置阻塞信号
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
sigset_t sigSet;
// 置空
sigemptyset(&sigSet);
// 阻塞 Ctrl + C
sigaddset(&sigSet, SIGINT);
// 阻塞 Ctrl + '\'
sigaddset(&sigSet, SIGQUIT);
// 试验看能否阻塞 SIGKILL 信号
sigaddset(&sigSet, SIGKILL);
sigprocmask(SIG_BLOCK, &sigSet, NULL);
pid_t pid = getpid();
while (true)
{
// 输出自己的pid, 方便测试 kill -SIGKILL
printf("pid = %d\n", pid);
// 获取未决信号集
sigset_t pend;
sigpending(&pend);
// 输出前31号,1为未决
for (int i = 1; i < 32; ++i)
{
printf("%d", sigismember(&pend, i));
}
sleep(1);
printf("\n");
}
}
这里也验证了, SIGKILL
信号是无条件终结进程的, 不可阻塞
/**
* @author IYATT-yx
* @brief 捕捉信号
*/
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
#include <unistd.h>
void info(int no)
{
if (no == SIGINT)
{
printf("捕捉到信号 SIGKILL\n");
}
}
int main(void)
{
// 第二个参数为回调函数
signal(SIGINT, info);
while (true)
{
printf("检测 Ctrl + C 中...\n");
sleep(1);
}
}
可以将这里的捕捉信号改为 SIGKILL , 也可以验证这个信号不可捕捉,程序会被杀死.
即不可阻塞,不可捕捉,任何进程收到 SIGKILL 都会被无条件的杀死.
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// sa_flags 为 0 ,则使用第一个
// sa_flags 为 1 ,则使用第二个
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
/**
* @author IYATT-yx
* @brief sigaction 捕捉信号
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <stdbool.h>
#include <unistd.h>
void info(int no)
{
if (no == SIGINT)
{
printf("捕捉到信号 SIGKILL\n");
}
}
int main(void)
{
struct sigaction act;
act.sa_flags = 0;
// 可以对 act.sa_mask 设置实现临时屏蔽某个信号,待回调函数执行完之后再处理这个信号,这里未示例,直接清空
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_handler = info;
sigaction(SIGINT, &act, NULL);
while (true)
{
sleep(1);
}
}
signal
只有第一次捕捉到设定的信号执行回调函数,之后按默认处理.
sigaction
每次捕捉到设定的信号都是执行回调函数