目录
一、定义
1.1概念
进程是一个独立的资源分配单元,不同用户进程之间的资源是相互独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。但是不同进程之间需要进行信息交互和状态传递等,所以产生了进程间通信。
1.2目的
- 数据传输:一个进程中的数据发送给另一个进程
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件(如进程终止时要通知父进程)。
- 资源共享:多个进程之间共享同样的资源。为此,需要内核提供互斥和同步(同步是按照任务的顺序执行任务,前一个任务没有执行结束,下一个任务不会执行)机制。
- 进程控制:有些进程希望完全控制另一进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够即使知道它的状态改变。
1.3 Linux操作系统支持的主要进程间通信机制
Q:Unix通信方式和另两个有啥区别?
A:
Q:System V 和 POSIX通信方式有啥区别,为啥归到一类?
A:
二、管道通信方式
2.1 定义
管道也叫无名管道,它是UNIX系统IPC最古老的形式,所以UNIX系统都支持这种通信机制。管道的本质是一块内核缓冲区。管道的读端和写端默认都是阻塞的。
2.2 管道特点
- 半双工:数据在同一时刻只能在一个方向上流动。
- 单方向:数据只能从管道的一端写入,从另一端读出。
- 先入先出:写入管道中的数据遵循先入先出的规则。
- 无格式:管道传送的数据是无格式的,要求管道的读出方式写入方必须事先约定好的数据格式(如多少字节算一个消息等)。
- 存于内存:管道不是普通文件,不属于文件系统,只存在内存中。
- 管道缓冲区:管道在内存中对应一个缓冲区。不同系统其大小不一定相同。
- 一次性读取:数据一旦被读走,该数据就被抛弃了,占用的空间也被释放。
- 具有公共祖先进程间使用:父进程与子进程,或者两个兄弟进程,具有亲缘关系的情况。
2.3 理解管道
类比现实中的管子,管子一端塞东西,一端取东西。
管道是一种特殊类型文件,在应用层体现为两个文件描述符。
Q:fd[0] 和fd[1]分别要填什么,哪个是读端,哪个是写端呢?
A:都可以设置。
开辟了管道之后如何实现两个进程间的通信呢?比如可以按下面的步骤通信。
1. 父进程调用 pipe 开辟管道,得到两个文件描述符指向管道的两端。
2. 父进程调用 fork 创建子进程,那么子进程也有两个文件描述符指向同一管道。
3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,
管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
2.3 管道相关函数
int pipe(int pipefd[2]);
功能:创建无名管道。
参数:pipefd:为int 型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。
当一个管道建立时,它会创建两个文件描述符 fd[0] 和 fd[1]。
其中fd[0]固定用于读管道,而fd[1]固定用于写管道。
一般文件I/O的函数都可以用来操作管道(lseek() 除外)。
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>
std::string callcmd(const std::string& cmdline)
{
constexpr int PipeBuffSize = 4096;
std::string pipe_str;
char* pipe_buffer = std::make_unique<char[]>(PipeBuffSize); // 包含4096个元素的数组
constexpr static int PIPE_READ = 0;
constexpr static int PIPE_WRITE = 1;
int pipe_in[2] = { 0 };
int pipe_out[2] = { 0 };
int pipe_in_ret = pipe(pipe_in);
int pipe_out_ret = pipe(pipe_out); // 创建两个管道
if (pipe_in_ret != 0 || pipe_out_ret != 0) { // 成功返回0
return {};
}
fcntl(pipe_out[PIPE_READ], F_SETFL, O_NONBLOCK); // 设置pipe_out管道为非阻塞
int exit_ret = 0;
int child = fork();
if (child == 0) {
// child process
dup2(pipe_in[PIPE_READ], STDIN_FILENO); // 通过 oldfd 复制出一个新的文件描述符 newfd,如果成功,newfd 和函数返回值是同一个返回值,最终 oldfd 和新的文件描述符 newfd 都指向同一个文件。
dup2(pipe_out[PIPE_WRITE], STDOUT_FILENO);
dup2(pipe_out[PIPE_WRITE], STDERR_FILENO);
// all these are for use by parent only //子类既不能读也不能写,都关了
close(pipe_in[PIPE_READ]);
close(pipe_in[PIPE_WRITE]);
close(pipe_out[PIPE_READ]);
close(pipe_out[PIPE_WRITE]);
exit_ret = execlp("sh", "sh", "-c", cmdline.c_str(), nullptr);
exit(exit_ret); // ?
}
else if (child > 0) {
// parent process
// close unused file descriptors, these are for child only
close(pipe_in[PIPE_READ]); // in写 out读
close(pipe_out[PIPE_WRITE]);
do {
ssize_t read_num = read(pipe_out[PIPE_READ], pipe_buffer.get(), PipeBuffSize); // out 循环读, get是获取智能指针的普通指针。
while (read_num > 0) {
pipe_str.append(pipe_buffer.get(), pipe_buffer.get() + read_num); // 循环放在pipe_str字符串中
read_num = read(pipe_out[PIPE_READ], pipe_buffer.get(), PipeBuffSize);
};
} while (::waitpid(child, &exit_ret, WNOHANG) == 0); // 若指定的子进程没有结束,则waitpid()函数返回0
close(pipe_in[PIPE_WRITE]);
close(pipe_out[PIPE_READ]);
}
else {
// failed to create child process
close(pipe_in[PIPE_READ]);
close(pipe_in[PIPE_WRITE]);
close(pipe_out[PIPE_READ]);
close(pipe_out[PIPE_WRITE]);
}
return pipe_str;
}
代码功能:子进程把命令结果给到父进程的字符串中。
Q:wait(NULL)什么意思?
A:该进程等待另一个进程,如果父进程不等待子进程,结束了,那么子进程就会变成僵尸进程。
僵尸进程:
进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
这样就会导致一个问题,如果进程不调用wait() 或 waitpid() 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
避免僵尸进程:
①可以使用wait()、waitpid(),但是这种方法会导致父进程等待。
②如果父进程要处理的事情很多,不能够挂起,通过signal()人为处理SIGCHLD信号,只要有子进程退出,那么自动调用指定好的回调函数。因为子进程结束后,父进程会收到SIGCHLD信号,可以在其回调函数中调用wait()或waitpid()回收子进程。
③如果父进程不关心子进程何时结束,那么可以调用signal(SIGCHLD,SIG_IGN)通知内核,自己对子进程的结束不感兴趣,父进程忽略此信号,那么子进程结束后,内核会回收, 并不再给父进程发送信号。
2.4 管道的读写特点(阻塞I/O)
- 写端没关闭,且管道无数据,读管道时阻塞。
- 写端没关闭,管道有数据,读管道读完数据了,会阻塞等写端。
- 写端关闭,读进程读管道所有内容后,返回0。
- 读端没关闭,写端写满了之后会阻塞,等读管道。
- 所有读端被关闭,写管道进程会收到信号SIGPIPE(表示不能写进去),然后退出。
2.5 设置为非阻塞的方法
设置读端非阻塞方法:
int flags = fcntl(fd[0], F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
或fcntl(fd[0], F_SETFL, O_NONBLOCK);
Q:fcntl()是什么函数?
A:功能:改变已打开的文件性质,fcntl针对描述符提供控制
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>
#include <fcntl.h>
int main(){
int fd_pipe[2] = {0};
pid_t pid;
int flags = fcntl(fd_pipe[0], F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd_pipe[0], F_SETFL, flags);
if(pipe(fd_pipe) < 0){
perror("pipe");
}
pid = fork();
if(pid == 0){
char buf[] = "I am mike";
write(fd_pipe[1], buf, strlen(buf));
_exit(0);
}
else if (pid > 0){
wait(NULL);
char str[50] = {0};
read(fd_pipe[0], str, sizeof(str));
printf("str = [%s]\n", str);
}
printf("我是父进程的输出内容");
return 0;
}
在这里设置了读端非阻塞,那么读完之后的进程就会关闭,但是写进程还没关闭,那么写进程就会收到SIGPIPE信号,然后写进程(子进程)就会异常,然后导致退出,就无法输出 "我是父进程的输出内容"这个字符串了。
2.6 查看管道缓冲区大小的函数
- 查看命令:ulimit -a
- 函数: long fpathconf(int fd, int name);
- 功能:该函数可以通过name参数查看不同的属性。
- 参数name:
-
_PC_PIPE_BUF 查看管道缓冲区大小
-
_PC_NAME_MAX 文件名字节数的上限
- 返回值:失败返回-1
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
int fd[2];
int ret = pipe(fd);
if(ret == -1){
perror("pipe error");
exit(1);
}
long num = fpathconf(fd[0], _PC_PIPE_BUF);
printf("num = %ld\n", num);
return 0;
}
返回的num为管道缓冲区的字节数(4096),也就是4KB。我们可以看到我们在函数中用的是fd[0],因该是用来读管道内容的。
三、有名管道
3.1定义
为了克服无名管道中只有亲属关系的进程可以通信,以及无名字的特性,创建了有名管道(FIFO)。有名管道在文件系统中作为特殊的存在,它还有名字,不相关的进程可以通信。
3.2有名管道与无名管道区别
有名管道有名字,不相关的进程可以打开命名管道进行通信。
有名管道FIFO文件在文件系统中,内容在内存中。
有名管道进程退出后,FIFO文件还保留在文件系统中,以便后续使用。
3.3注意
- 当一个进程只读时,它会被阻塞,直到只写进程参与。
- 当一个进程只写时,它也会被阻塞,直到只读进程参与。
四、共享存储映射
4.1概述
存储映射I/O使一个磁盘文件与存储空间中的缓冲区相映射。
当读取文件内容时,就在读取文件的存储映射部分,而要写入文件时也就写入进程的地址空间中的文件的存储映射部分,它会自动的被写入文件中,这样在不调用write和read函数时,使用指针完成了I/O操作。
共享内存是最有用的进程间通信方式。也是最快的IPC方式,因为进程可以直接读写内存,不需要任何的数据拷贝。
4.2相关函数
#include <sys/mman.h>
mmap():
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:
一个文件或者其它对象映射进内存中
参数:
addr: 指定映射的起始地址,通常设置为NULL,由系统指定
length:映射到内存的文件长度
prot:映射区的保护方式,最常用的:
a)读:PROT_READ
b) 写:PROT_WRITE
c) 读写:PROT_READ | PROT_WRITE
flags: 映射区的特性,可以是
a)MAP_SHARED:写入应摄取的数据会复制回文件,且允许其它映射该文件的进程共享。
b) MAP_PRIVATE:对映射区的写入操作会产生映射区的复制,对此区域所做的修改不会写回原文件。
fd:由open返回的文件描述符,代表要映射的文件。
offset:以文件开始处的偏移量,必须是4k的整数倍,通常为0,表示从文件头开始映射。
返回值:
成功:返回创建的映射区首地址
失败:MAP_FAILED宏
int munmap(void *addr, size_t length);
功能:
释放内存映射区
参数:
addr:使用mmap函数创建的映射区首地址
length:映射区的大小
返回值:
成功:0
失败:-1
Q:为什么文件的偏移量必须是4K的整数倍?
A:内存部分内容:内存是页管理的,一页只能用4字节。
关于mmap函数的使用总结:
1) 第一个参数写成NULL
2) 第二个参数要映射的文件大小 > 0
3) 第三个参数:PROT_READ 、PROT_WRITE
4) 第四个参数:MAP_SHARED 或者 MAP_PRIVATE
5) 第五个参数:打开的文件对应的文件描述符
6) 第六个参数:4k的整数倍,通常为0
4.3 示例
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
int main(){
int fd = open("123213.txt", O_RDWR | O_CREAT);
if(fd== -1){
perror("open error");
close(fd);
exit(1);
}
int len = lseek(fd, 0, SEEK_END);
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED){
perror("mmap error");
close(fd);
exit(1);
}
close(fd);
char buf[4096];
printf("content:%s\n", buf);
strcpy((char*)buf, "I love China");
int ret = munmap(ptr, len);
if(ret == -1){
perror("munmap error");
close(fd);
exit(1);
}
close(fd);
return 0;
}
注:代码在运行到mmap函数时,报错mmap error: Invalid argument,现在原因不明。
4.4匿名实现父子进程通信
普通的共享存储映射中,需要依赖一个临时文件,在使用mmap()时,必须首先创建一个临时文件,当使用munmap时又要关闭文件,比较麻烦,所以可以使用匿名映射区的方法。
匿名映射区:无需依赖文件就可以创建映射区,需要借助标志位flags。
使用MAP_ANONYMOUS(或MAP_ANON)。 --Linux特有,UNIX中无。
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <wait.h>
int main(){
int len = 4096;
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
if (ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
// 创建子进程
pid_t pid = fork();
if (pid > 0) //父进程
{
// 写数据
strcpy((char*)ptr, "hello mike!!");
// 回收
wait(NULL);
}
else if (pid == 0)//子进程
{
sleep(1);
// 读数据
printf("%s\n", (char*)ptr);
}
// 释放内存映射区
int ret = munmap(ptr, len);
if (ret == -1)
{
perror("munmap error");
exit(1);
}
return 0;
}
6.1练习
void processA(){
int pid = fork();
int pipe1 = mkfifo("./pipe1", O_RDONLY);
int pip2 = mkfifo("./pipe2", O_WRONLY);
if(pid > 0){
int fd1 = open("./pipe1", O_RDONLY);
int fd2 = open("./pipe2", O_WRONLY);
while(1){
//write
char wr[100] = "hello";
write(fd2, wr, strlen(wr));
char recv[100] = {0};
read(fd1, recv, sizeof(recv));
printf("read [%s]\n", recv);
}
close(fd1);
close(fd2);
}
else if(pid == 0){
int fd1 = open("./pipe1", O_RDONLY);
int fd2 = open("./pipe2", O_WRONLY);
while(1){
//write
char wr[100] = "hello2333";
write(fd2, wr, strlen(wr));
//read
char recv[100] = {0};
read(fd1, recv, sizeof(recv));
printf("read [%s]\n", recv);
}
五、信号
5.1定义
信号是通过软件中断,也就是说它是在软件中对中断机制的一种模拟,是一种异步通信方式。信号可以让一个进程被另一个进程所打断,转而处理某一个突发事件。
中断”在我们生活中经常遇到,譬如,我正在房间里打游戏,突然送快递的来了,把正在玩游戏的我给“中断”了,我去签收快递( 处理中断 ),处理完成后,再继续玩我的游戏。
5.2信号中断的原理
信号可以直接在用户空间和内核空间之间进行交互。内核空间通过信号告诉用户空间系统发生了哪些事件。
一个完整的信号周期包括三个部分:信号的产生、信号的注册和注销、执行信号处理函数。
前两个部分都是信号的内部机制,只有信号处理是函数的执行。
Q:为什么在信号处理之前分别要进行注册和注销?
A:
5.3 信号四要素
分别为:①编号 ②名称 ③事件 ④默认处理动作
信号的默认处理动作Action:
Term:终止进程
Stop:暂停或停止进程
Cont:继续执行进程
Ign:忽略信号(默认即时对该种信号忽略操作)
Core: 终止该进程,并生成Core文件。(查验死亡原因,用于gdb调试)
这里特别强调了9) SIGKILL 和19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
5.4信号产生函数
5.4.1 kill函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能:给指定进程发送指定信号
参数:
pid = 0 将信号传给当前进程组的所有进程
pid > 0 将信号传给pid的进程
pid = -1 将信号传给系统内的所有进程
pid < -1 将信号传给指定进程组的所有进程,该进程组号为pid的绝对值
返回值:
成功: 0
失败: -1
5.4.2 raise函数
#include <signal.h>
int raise(int sig);
功能:给当前进程发送指定信号
5.4.3 abort函数
#include <stdlib.h>
void abort();
功能:给自己发送异常终止信号 SIGABORT,并产生Core文件
5.4.4 alarm函数
#include <unistd.h>
unsigned int alarm(unsigned int second);
功能:设置定时器,当时间到时,内核会给当前进程发送SIGALRM信号。进程收到信号后默认终止。每个进程都只有一个唯一的定时器。
参数:
取消定时器,就会返回剩余的秒数。否则返回0
5.4.5 setitimer函数
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:设置定时器,可以代替alarm函数。精度为us,可以实现周期定时。
参数:
which表示指定定时方式
a) 自然定时 ITIMER_REAL-》SIGALRM计算自然时间
b) 虚拟空间计时(用户空间)ITIMER_VIRTUAL-》SIGVTALRM只计算进程占用CPU时间
c) 运行时计时(用户+内核):ITIMER_PROF-》SIGPROF计算占用CPU及执行系统调用的时间
new_value: struct itimerval,负责设定timeout时间
new_value:struct itimerval
struct itimerval{
struct itimerval{
struct timerval it_interval; //闹钟触发周期
struct timerval it_val; //闹钟触发时间
};
struct timeval{
long tv_sec; //秒
long tv_usec; //微秒
};
itimerval.it_value: 设定第一次执行function所延迟的秒数
itimerval.it_interval: 设定以后每几秒执行function
old_value: 存放旧的timeout值,一般指定为NULL
5.5、信号集
共两个信号集,一个叫做阻塞信号集、一个叫做未决信号集。
这两个信号都是内核使用位图机制来实现的,内核不允许我们直接修改信号集,所以定义了一个新集合,我们通过使用信号集操作函数对PCB中的两个信号集做处理。
Q:位图机制是什么?
A:一位表示一个状态,节省空间,而不是一个int表示一个状态。
阻塞信号集:将某些信号加入信号集,如果X信号加入信号集后被设置为屏蔽状态,那么再收到这个信号时,对该信号的处理将被延后,直到该信号状态不再是屏蔽状态。
未决信号集:当信号被阻塞合被屏蔽时,信号处于未决状态。在未决状态时,该信号的位置被翻为1,当信号被处理时,信号位置就会被翻回0。
5.6自定义信号集函数
5.6.1添加、删除函数
#include <signal.h>
int sigemptyset(sigset_t *set); //将set集合置空
int sigfillset(sigset_t *set); //将所有信号加入set集合
int sigaddset(sigset_t *set, int signo); //将signo信号加入到set集合
int sigdelset(sigset_t *set, int signo); //从set集合中移除signo信号
int sigismember(const sigset_t *set, int signo); //判断信号是否存在
5.6.2.sigprocmask函数
创建子进程时,子进程将继承父进程的阻塞集。
修改当前信号掩码改变信号阻塞状态。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。
参数:
how : 信号阻塞集合的修改方法,有 3 种情况:
SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。相当于 mask = mask|set。
SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。相当于 mask = mask & ~ set。
SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。相当于mask = set。
set:要操作的信号集地址。
若set为NULL,则不改变信号阻塞集,函数只把信号阻塞集保存到oldset中。
返回值:
成功:0
失败:-1,失败时错误代码只能是EINVAL,表示参数how不合法。
5.6.3.sigpending函数
#include <signal.h>
int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:set:未决信号集
5.7、信号捕捉
5.7.1.信号处理方式
一般信号会有3种处理方法:
①默认处理方式:大多数信号会让系统终止当前进程。
②忽略信号:接收到此信号无任何动作。
③自定义信号处理:用户自定义函数处理信号。
注意:SIGKILL和SIGSTOP是终止该进程,不能改变信号处理方式。
内核实现信号捕捉过程:
Q:内部详细机制还不清楚?
A:
5.7.2.signal函数
#include<signal.h>
typedef void(*sighanler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:
注册信号处理函数,确定收到信号后的处理函数的入口地址。此函数不会阻塞。
参数:
signum:信号编号,可以是数字和信号宏定义,可通过kill -l查看。
handler:3种情况。SIG_IGN(忽略)、SIG_DFL(执行系统默认动作)、信号处理函数名。
返回值:
成功:第一次返回NULL,下一次返回信号上一次注册的信号处理函数的地址。
失败:返回SIG_ERR
在此函数种,由于UNIX和LINUX之间可能由不同的行为,所以要尽量避免,应采用sigaction函数。
示例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void signal_handler(int signo){
if(signo == SIGINT){
printf("recv SIGINT\n");
}else if(signo == SIGQUIT){
printf("recv SIGQUIT\n");
}
}
int main(){
printf("wait for SIGINT or SIGQUIT\n");
signal(SIGINT, signal_handler);
signal(SIGQUIT, signal_handler);
while(1);
return 0;
}
5.7.3.sigaction函数
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
检查、修改信号的设置。
参数:
signum:待操作的信号。
act:要设置的对新信号的处理方式。
oldact:原来对信号的处理方式。
如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。
sigaction结构体:
struct sigaction{
void(*sa_handler)(int);//旧的信号处理函数指针
void(*sa_sigaction)(int, siginfo_t *, void *);//新的信号处理函数指针
sigset_t sa_mask;//信号阻塞集
int sa_flags;//信号处理方式
void(*sa_restorer)(void);//已弃用
};
1) sa_handler、sa_sigaction:信号处理函数指针,和 signal() 里的函数指针用法一样,应根据情况给sa_sigaction、sa_handler 两者之一赋值,其取值如下:
a) SIG_IGN:忽略该信号
b) SIG_DFL:执行系统默认动作
c) 处理函数名:自定义信号处理函数
2) sa_mask:信号阻塞集,在信号处理函数执行过程中,临时屏蔽指定的信号。
3) sa_flags:用于指定信号处理的行为,通常设置为0,表使用默认属性。它可以是一下值的“按位或”组合:
Ø SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)
Ø SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
Ø SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
Ø SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
Ø SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
Ø SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
信号处理函数:
void(*sa_sigaction)(int signum, siginfo_t *info, void *context);
参数说明:
signum:信号编号
info:记录信号发送进程信息的结构体
context:可以赋给指向ucontext_t类型的一个对象指针,以引用在传递信号时被中断的接收的进程或线程的上下文。
5.7.4.sigqueue函数
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
功能:
给指定进程发送信号。
参数:
pid:进程号
sig:信号编号
value:通过信号传递的参数
union sigval{
int sigval_int;
void *sigval_ptr;
}
向指定进程发送指定信号的同时,携带数据。但如传地址,需注意,不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义。
不可重入、可重入函数:
如果不同任务调用这个函数时可能修改其它任务调用这个函数的数据,从而导致出不可预料的后果,那么当前函数称为不安全函数,也叫做不可重入函数。
不可重入函数条件:
- 函数体内使用了静态数据结构
- 函数体内调用了malloc()或free()
- 函数体使用了标准I/O函数
可重入函数:可以被多个任务调度过程中,任务在调用时不必担心数据会出错。
保证函数可重入性方法:
- 在写函数时尽量使用局部变量
- 对于要使用的全局变量加以保护(如关中断、信号量等互斥方法)
六、消息队列
6.1.概念:加强型的管道,他可以指定存放的数据类型,与取走的数据类型,方便不同进程取数据。
6.2.相关函数:
查看system V (5)版本的通信对象命令:
查看消息队列: ipcs -q
查看共享内存: ipcs -m
查看信号量: ipcs -s
查看全部:ipcs -a
删除消息队列:
ipcrm -q (对象ID)
ipcrm -Q (键值)
消息队列的信息
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
(KEY值)(消息队列通信对象)
//如何创建消息队列
1.创建KEY值
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);//proj_id ->不能超过255
2.获取消息队列通信对象ID
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数一:键值key
参数二:权限
IPC_CREAT 创建
IPC_EXCL 检查是否存在
mode 0666
返回值:成功 返回 对象ID
失败 返回 -1
3.进行数据的交互
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//发送
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数一:需要发送的消息队列ID
参数二:数据缓存区
参数三:数据的大小
参数四: 是否阻塞
0 阻塞
IPC_NOWAIT 不阻塞
返回值: 成功返回 0
失败返回 -1
PS,PS,PS 注意!!!!
数据的缓冲区必须要定义为如下结构体:
struct msgbuf {
long mtype; /*数据类型必须大于0, must be > 0 */
char mtext[1]; /*数据,该数组的长度任意定义*/
};
//读取
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//参数一:对象ID
//参数二:数据的缓存区
//参数三:需要获取的数据大小
//参数四:数据的类型
参数五: 是否阻塞
0 阻塞
IPC_NOWAIT 不阻塞
返回值: 成功返回 读到的数据大小
失败返回 -1
4.销毁消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数一:对象ID
参数二:控制命令 IPC_RMID -》删除
参数三:设置 获取的时候使用
返回值:成功 0
失败 -1
示例:
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#define _USE_GNU
#include<sys/msg.h>
#include<stdlib.h>
#include<string.h>
struct msgbuf2{ // 数据缓冲区
long mtype;
char mtext[500];
};
int main(int argc,char *argv[])
{
int queue_id;
msgbuf2 * msg,*recv_msg;
int rc;
int msgsz;
queue_id=msgget(IPC_PRIVATE,IPC_CREAT|0600); // 创建消息队列
if(queue_id==-1){
perror("main's msgget");
exit(1);
}
printf("the created message's id ='%d'.\n",queue_id);
//分配一个消息结构"Hello World!"
msg = (struct msgbuf2 *)malloc(sizeof(struct msgbuf2)+strlen("hello,world"));
msg -> mtype = 1;//消息队列的索引赋值为1
strcpy(msg -> mtext,"hello world");//将字符串复制到消息体中
//发送消息
rc = msgsnd(queue_id,msg,strlen(msg -> mtext) + 1,0); // 进行数据的交互
//这里的+1是指字符串的结束符
if(rc == -1){
perror("msgsnd");
exit(1);
}
else
printf("%d\n",rc);
free(msg);//释放消息占用的空间
printf("message is placed on message's queue\n");
//接收消息
recv_msg = (struct msgbuf2 *)malloc(sizeof(struct msgbuf2)+strlen("hello world")+1);
msgsz = strlen(recv_msg -> mtext) + 1;
rc = msgrcv(queue_id,recv_msg,msgsz,0,0); // 读取消息
if(rc == -1){
perror("msgrcv");
exit(1);
}
printf("received message's mtype is'%ld';mtext if '%s' \n",recv_msg->mtype,recv_msg->mtext);
msgctl(queue_id,IPC_RMID,NULL);//删除消息队列
return 0;
}
七、信号量
概念:本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。
相关函数流程:
sem_init,创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化
sem_wait信号量P操作(减1)
sem_post信号量V操作(加1)
sem_getvalue获取信号量的值
sem_destroy函数删除 sem 标识的信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:
创建一个信号量并初始化它的值。一个无名信号量在被使用前必须先初始化。
参数:
sem:信号量的地址。
pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
value:信号量的初始值。
返回值:
成功:0
失败: - 1
int sem_destroy(sem_t *sem);
功能:
删除 sem 标识的信号量。
参数:
sem:信号量地址。
返回值:
成功:0
失败: - 1
int sem_wait(sem_t *sem);
功能:
将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作。
参数:
sem:信号量的地址。
返回值:
成功:0
失败: - 1
int sem_trywait(sem_t *sem);
以非阻塞的方式来对信号量进行减 1 操作。
若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
限时尝试将信号量的值减 1
abs_timeout:绝对时间
struct timespec {
time_t tv_sec; /* seconds */ // 秒
long tv_nsec; /* nanosecondes*/ // 纳秒
}
time_t cur = time(NULL); //获取当前时间。
struct timespec t; //定义timespec 结构体变量t
t.tv_sec = cur + 1; // 定时1秒
sem_timedwait(&cond, &t);
int sem_post(sem_t *sem);
功能:
将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())。
参数:
sem:信号量的地址。
返回值:
成功:0
失败:-1
int sem_getvalue(sem_t *sem, int *sval);
功能:
获取 sem 标识的信号量的值,保存在 sval 中。
参数:
sem:信号量地址。
sval:保存信号量值的地址。
返回值:
成功:0
失败:-1
示例:
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
#include<unistd.h>
sem_t sem; //信号量
void printer(char *str)
{
sem_wait(&sem);//减一
while (*str)
{
putchar(*str); // 向终端输出一个字符
fflush(stdout); // 强制马上输出,避免错误
str++;
sleep(1);
}
printf("\n");
sem_post(&sem);//加一
}
void *thread_fun1(void *arg)
{
char *str1 = "hello";
printer(str1);
}
void *thread_fun2(void *arg)
{
char *str2 = "world";
printer(str2);
}
int main(void)
{
pthread_t tid1, tid2;
sem_init(&sem, 0, 1); //线程间共享,初始化信号量,初始值为 1
//创建 2 个线程
pthread_create(&tid1, NULL, thread_fun1, NULL);
pthread_create(&tid2, NULL, thread_fun2, NULL);
//等待线程结束,回收其资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&sem); //销毁信号量
return 0;
}