1 进程间通信常见方法
内核提供的这种机制称为进程间通信(IPC, InterProcess Communication)
常见方式:
1. 管道 pipe(使用最简单)
2. 信号 signal(开销最小)
3. 共享映射区 mmap(无血缘关系)
4. 本地套接字 socket(最稳定)
2 管道
最简单的IPC机制,作用于有血缘关系的进程之间,调用pipe系统函数即可创建一个管道
管道的特性:
1. 其本质是一个伪文件(实为内核缓冲区)
2. 由两个文件描述符引用,一个表示读端,一个表示写端。
3. 规定数据从管道的写端流入管道,从读端流出。
管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:。
1. 数据不能进程自己写,自己读。
2. 管道中数据不可反复读取。一旦读走,管道中不再存在。
3. 采用半双工通信方式,数据只能在单方向上流动。
4. 只能在公共祖先的进程间使用管道
2.1 pipe函数
创建并打开管道
int pipe(int pipefd[2]);
参数:
pipefd[0]:读端
pipefd[1]:写端
返回值:
成功:0
失败:-1
示例:
pid = fork();
if(pid >0){
close(fd[0]); // 父进程关闭读段
write(fd[1],str,strlen(str));
close(fd[1]);
}else if(pid ==0){
close(fd[1]); // 子进程关闭写段
ret = read(fd[0],buf, sizeof(buf));
write(STDOUT FILENO, buf, ret);
close(fd[0]);
}
管道的读写行为:
读管道:
1、管道有数据,read返回实际读到的字节数
2、管道无数据
1、无写端打开,read返回0
2、有写端打开,read阻塞等待
写管道:
1、管道读端全部关闭,进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
2、管道读端没有全部关闭
1、管道已满,write阻塞
2、管道未满,write写数据,并返回实际写入字节数
2.2 管道缓冲区大小
可以使用 ulimit-a命令来査看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:
pipe size (512 bytes, -p)8
也可以使用 fpathconf函数,借助参数选项来查看。使用该宏应引入
long fpathconf(int fd,int name);
参数:
fd:读端或写端
name:
_PC_PIPE_BUF:管道大小
返回值
成功:返回管道的大小
失败:-1,设置 erron
头文件
#include <unistd.h>
2.3 FIFO
命名管道,解决没有血缘关系的进程之间进行通信
int mkfifo(const char *pathname, mode_t mode);
参数:
pathname:管道名
mode:权限
返回值
成功:0
失败:-1,设置 erron
示例:
mkfifo("myfifo",0644);
使用fifo管道相当于使用文件,读文件与写文件
3 共享存储映射
3.1 文件进程间通信
有血缘关系的进程之间,共享文件描述符,可以打开共享文件
无血缘关系的进程之间,知道文件路径也可以共享文件
3.2 存储映射I/O
使磁盘文件与存储空间的一个缓冲区相映射
3.2.1 mmap函数
创建一个共享内存映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
addr:指定映射区的首地址,通常为 NULL,让系统自动分配
length:共享内存映射区的大小,通常 <=文件实际大小
prot:共享内存映射区的读写属性
PROT_READ 读权限
PROT_WRITE 写权限
PROT_READ | PROT_WRITE 读写权限
flags:标注共享内存的共享属性
MAP_SHARED 共享
MAP_PRIVATE 私有
fd:用于创建共享内存映射区的那个文件的 文件描述符
offset:偏移位置,必须是 4K 的整数倍。默认0表示映射文件全部
返回值:
成功:返回共享内存映射区的首地址
失败:MAP_FAILED ((void *) -1)
int munmap(void *addr, size_t length);
返回值:
成功:0
失败:-1
头文件:
#include <sys/mman.h>
示例:
p = (char*)mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
mmap注意事项
- 用于创建映射区的文件大小为 0,而实际指定 非0 大小创建映射区,报 总线错误
- 用于创建映射区的文件大小为 0,而实际指定 0 大小创建映射区,报无效参数
- 用于创建映射区的文件权限为 只读,映射区属性为 读写,报无效参数
- 创建映射区需要 读权限,因此mmap的读写权限应该 <= 文件的open权限
- 文件描述符fd,在mmap创建映射区完成之后可以关闭。后续访问文件用地址访问
- offset 必须是 4K 的整数倍(MMU映射的最小单位4K)
- 对申请的映射区内存,不能越界访问
- munmap用于释放的地址必须是mmap申请返回的地址,不能自增
- 映射区共享属性为MAP_PRIVATE ,对内存所做的所有修改只在内存中有效,在磁盘中无效
- 映射区共享属性为MAP_PRIVATE,只需要open文件时有读权限,用于创建映射区即可
mmap函数保守调用方式:
- open(O_RDWR)
- mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
3.2.2 mmap父子进程间通信
- 父进程 先创建映射区。open(O_RDWR) mmap(MAP_SHARED)
- 指定MAP_SHARED权限
- fork()创建子进程
- 一个进程读,一个进程写
3.2.3 mmap无血缘关系进程间通信
- 两个进程打开同一个文件,创建映射区
- 指定flag为MAP_SHARED
- 一个进程写入,一个进程读出
注意:无血缘关系间通信,mmap:数据可以重复读取;fifo:数据只能读取一次
3.2.4 匿名映射
无需创建文件进行映射,设置prot 参数 MAP_ANON
p = (char*)mmap(NULL, 随意设置, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
4 信号
每个进程收到的信号,都是由负责发送的,并由内核处理
产生信号:
- 按键产生:Ctrl+c
- 系统调用产生:kill、raise
- 软件条件产生:定时器
- 硬件异常产生:非法访问内存(段错误)
- 命令产生:kill命令
信号处理方式:
- 默认动作
- 忽略
- 捕捉(用户自定义)
**阻塞信号集(信号屏蔽字)😗*将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
未决信号集:
- 信号产生,未决信号集中描述该信号的位立刻翻转为 1,表信号处于未决状态。当信号被处理对应位翻转回为 0。这一时刻往往非常短暂。
- 信号产生后由于*某些原因(主要是阻塞)*不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前。信号一直处于未决状态。
4.1 信号四要素
1、编号 2、名称 3、事件 4、默认处理动作
可以通过man 7 signal查看
常规信号:
- SIGHUP: 当用户退出 shell 时,由该shel!启动的所有进程将收到这个信号,默认动作为终止进程。
- SIGINT: 当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动。作为终止进程。
- SIGQUIT: 当用户按下<Ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信。默认动作为终止进程号。
- SIGILL: CPU 检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件。
- SIGTRAP: 该信号由断点指令或其他 trap 指令产生。默认动作为终止里程并产生 core 文件。
- SIGABRT: 调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件。
- SIGBUS: 非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件。
- SIGFPE: 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生 core 文件。
- SIGKILL: 无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
- SIGUSR1: 用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
- SIGSEGV: 指示进程进行了无效内存访问。默认动作为终止进程并产生 core 文件。
- SIGUSR2: 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程
- SIGPIPE: Broken pipe 向一个没有读端的管道写数据。默认动作为终止进程。
- SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。
- SIGTERM: 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell 命令Ki 时,缺省产生这个信号。默认动作为终止进程。
- SIGSTKFLT: Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
- SIGCHLD: 子进程状态发生变化时,父进程会收到这个信号。默认动作为忽略这个信号。
- SIGCONT: 如果进程已停止,则使其继续运行。默认动作为继续/忽略。
- SIGSTOP: 停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
- SIGTSTP: 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
- SIGTTIN: 后台进程读终端控制台。默认动作为暂停进程。
- SIGTTOU: 该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
- SIGURG: 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
4.2 信号的产生
4.2.1 终端按键产生信号
- Ctrl + x ——> SIGINT(终止)
- Ctrl + z ——> SIGSTOP(暂停)
- Ctrl + \ ——> SIGQUIT(退出)
4.2.2 硬件异常产生信号
- 除0操作 ——> SIGFPE(浮点数除外)
- 非法访问内存——> SIGSEGV(段错误)
- 总线错误——> SIGBUS(终止)
4.2.3 kill函数和kill命令
给对应进程发送信号
int kill(pid_t pid, int sig);
参数:
pid:进程id
>0 指定进程
=0 调用kill函数进程所属同一进程组的所有进程
<0 取|pid|发给对应进程组
=-1 发给进程有权限发送的所有进程
sig:信号
SIGKILL
头文件:
#include <signal.h>
4.3 软件条件产生信号
4.3.1 alarm函数
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止
使用自然计时法,每个进程都有且只有唯一个定时器。
unsigned int alarm(unsigned int seconds);
返回:
0 或 剩余的秒数
无失败
常用:
取消定时器alarm(0),返回旧闹钟余下秒数
4.3.2 setitimer函数
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数:
which:指定定时方式
自然定时:ITIMER_REAL ——> SIGLARM 计算自然时间
虚拟空间计时(用户空间):ITIMER_VIRTUAL ——> SIGVTALRM 只计算进程占用cpu时间
运行时计时(用户+内核):ITIMER_PROF ——> SIGPROF 计算占用cpu和执行系统调用的时间
4.4 信号集操作函数
4.4.1 信号集设定
sigset_t set; 自定义信号集
int sigemptyset(sigset_t *set); 创建空的信号集
int sigfillset(sigset_t *set); 将所有信号置1
int sigaddset(sigset_t *set, int signum); 将一个信号添加到集合中
int sigdelset(sigset_t *set, int signum); 将一个信号从集合中删除
int sigismember(const sigset_t *set, int signum); 判断某个信号是否在集合当中.在 1,不在 0,错误 -1
头文件:
#include <signal.h>
4.4.2 sigprocmask 函数
设置信号屏蔽字,用来屏蔽信号、解除屏蔽
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how:假设当前的信号屏蔽字为mask
SIG_BLOCK:set表示需要屏蔽的信号,相当于 mask=mask|set
SIG_UNBLOCK:set表示需要解除屏蔽的信号,相当于 mask=mask&~set
SIG_SETMASK:set表示用于代替原始屏蔽集的新评比集,相当于 mask=set。若调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
set:自定义set
oldset:旧的mask
返回值:
成功:0
失败:-1
4.4.3 sigpending函数
读取未决信号集
int sigpending(sigset_t *set);
返回值:
成功:0
失败:-1
4.4.4 信号集操作示例
#include <bits/stdc++.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
using namespace std;
void sys_err(const char *str){
perror(str);
exit(1);
}
void print_set(sigset_t *set){
int i;
for(int i=1;i<32;i++){
if(sigismember(set,i))
cout<<"1";
else
cout<<"0";
}
cout<<endl;
}
int main(int argc,char *argv[]){
sigset_t set,oldset,newset;
int ret=0;
sigemptyset(&set);
sigaddset(&set,SIGINT);
sigaddset(&set,SIGQUIT);
ret=sigprocmask(SIG_BLOCK,&set,&oldset);
if(ret==-1){
sys_err("sigprocmask error");
}
while(1){
ret=sigpending(&newset);
if(ret==-1){
sys_err("sigpending error");
}
print_set(&newset);
sleep(1);
}
return 0;
}
4.5 信号捕捉
4.5.1 signal 函数
注册一个信号捕捉函数
typedef void (*sighandler_t)(int); sighandler_t为指向返回值为void,参数为int的函数指针
sighandler_t signal(int signum, sighandler_t handler);
4.5.2 sigaction 函数
注册一个信号捕捉函数
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
signum:信号表示
act:新的处理状态
oldact:旧的处理状态
struct sigaction {
void (*sa_handler)(int); 捕捉到后执行的函数
void (*sa_sigaction)(int, siginfo_t *, void *); 一般不用
sigset_t sa_mask; 只作用于信号捕捉函数执行期间
int sa_flags; 表示设置参数
void (*sa_restorer)(void); 废弃
};
返回值:
成功:0
失败:-1
示例:
#include <bits/stdc++.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
using namespace std;
void sys_err(const char *str){
perror(str);
exit(1);
}
void sig_catch(int signo){
cout<< "catch you: "<<signo<<endl;
return;
}
int main(int argc,char *argv[]){
struct sigaction act,oldact;
//赋初值
act.sa_handler=sig_catch; //设置捕捉信号后执行函数
sigemptyset(&act.sa_mask); //设置屏蔽字
act.sa_flags=0; //设置默认属性
int ret=sigaction(SIGINT,&act,&oldact); //注册信号捕捉函数
if(ret==-1){
sys_err("sigaction error");
}
while(1);
return 0;
}
4.5.3 信号捕捉特性
- 进程正常运行时,默认 PCB 中有一个信号屏蔽字,假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。
- xxx信号捕捉函数执行期间,xxx信号自动被屏蔽。(sa_flags = 0)
- 捕捉函数执行期间,被阻塞信号多次发送,接触屏蔽后只处理一次。(后 32个实时信号支持排队)。
4.5.4 内核实现捕捉过程
5 SIGCHLD信号
5.1 SIGCHLD产生条件
- 子进程终止时
- 子进程接收到SIGSTOP信号停止时
- 子程序在停止状态,接收到SIGCONT后唤醒时
5.2 借助 SIGCHLD 回收子进程
子进程结束运行,其父进程会收到 SIGCHLD 信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函
数中完成子进程状态的回收。
#include <bits/stdc++.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <wait.h>
using namespace std;
void sys_err(const char *str){
perror(str);
exit(1);
}
void catch_child(int signo){
pid_t wpid;
int status;
// while ((wpid=wait(NULL))!=-1)
// {
// cout<<"catch child id "<<wpid<<endl;
// }
//确保同时回收的子进程都能够回收,不会产生僵尸进程
while ((wpid=waitpid(-1,&status,0))!=-1)
{
if(WIFEXITED(status)){ //为真,说明子进程正常终止
cout<<"catch child id = "<<wpid<<", ret = "<<WEXITSTATUS(status)<<endl;
}
}
return;
}
int main(int argc,char *argv[]){
pid_t pid;
//阻塞
int i;
for(i=0;i<5;i++){
if((pid=fork())==0)
break;
}
if(5 == i){
struct sigaction act;
act.sa_handler=catch_child; //设置捕捉信号后执行函数
sigemptyset(&act.sa_mask); //设置屏蔽字
act.sa_flags=0; //设置默认属性
sigaction(SIGCHLD,&act,NULL);
//解除阻塞
cout<<"I am parent, pid = "<<getpid()<<endl;
}else{
cout<<"I am child, pid = "<<getpid()<<endl;
return i+1;
}
return 0;
}
中断系统调用
系统调用可分为两类: 慢速系统调用和其他系统调用。
- 慢速系统调用: 可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期); 也可以设定系统调用是否重启。如,read、write、pause、wait.
- 其他系统调用: getpid、getppid、fork…
结合 pause,回顾慢速系统调用:
慢速系统调用被中断的相关行为,实际上就是 pause的行为: 如,read.
- 想中断 pause,信号不能被屏蔽。
- 信号的处理方式必须是捕捉(默认、忽略都不可以)~
- 中断后返回-1, 设置
errno.为 EINTR(表“被信号中断”)
可修改 sa_flags 参数来设置被信号中断后系统调用是否重启。SA_INTERRURT不重启 SA_RESTART 重启。
进程组和会话
进程组:多个进程的集合
会话:多个进程组的集合
创建会话
创建一个会话需要注意以下6点注意事项:
- 调用进程不能是进程组组长,该进程变成新会话首进程(session header)
- 该进程成为一个新进程组的组长进程。
- 需有 root 权限(ubuntu 不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 该调用进程是组长进程,则出错返回。
- 建立新会话时,先调用 fork,父进程终止,子进程调用setsid()
getsid 函数
获取进程所属会话ID
pid_t getsid(pid_t pid);
参数:
pid:进程ID
pid 为 0 表示察看当前进程 sessionID.
返回值:
成功:返回调用进程的会话ID;
失败:-1,设置 errno
setsid函数
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。
pid_t setsid(void);
返回值:
成功:返回调用进程的会话ID;
失败:-1,设置 errng
调用了 setsid 函数的进程,既是新的会长,也是新的组长。
守护进程
Daemon(精灵)进程,是 Linux 中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
Linux 后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现; ftp服务器; nfs服务器等。
创建守护进程,最关键的一步是调用 setsid 函数创建一个新的 Session,并成为 Session Leader。
创建守护进程模型
- 创建子进程,父进程退出——fork()
- 在子进程中创建新会话——setsid()
- 改变当前目录——chdir()
- 重设文件权限掩码——umask()
- 关闭文件/重定向描述符
- 开始执行守护进程核心工作
示例代码:
#include <bits/stdc++.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
using namespace std;
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
pid_t pid;
int ret,fd;
pid=fork();
if(pid>0){ //父进程终止
exit(0);
}
pid=setsid(); //创建新会话
if(pid==-1)
sys_err("setpid error");
ret= chdir("/home/malinqian/code"); //改变工作目录
if(ret==-1)
sys_err("chdir error");
umask(0022); //改变文件权限掩码
close(STDIN_FILENO); //关闭文件描述符 0
fd=open("/dev/null",O_RDWR); //fd ——> 0
if(fd==-1)
sys_err("open error");
dup2(fd,STDOUT_FILENO); //重定向文件描述符 1 2
dup2(fd,STDERR_FILENO);
while(1); //模拟守护进程工作
return 0;
}