1. 共享内存通信
1.1 概述
共享内存,就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立刻印象到可以访问到同一共享内存的其他进程
1.2 共享内存原理
在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(AddrSpace),并且都有一个与之对应的“页表”,负责将进程的虚拟地址和物理地址进行映射,通过内存管理单元(MMU)。两个不同的虚拟地址通过页表映射到物理空间的同一区域,这个区域就是共享内存区域。
1.3 共享内存接口函数指令
(1)查看系统中的共享存储段
ipcs -m
(2)删除系统中的共享存储段
ipcrm -m [shmid]
(3)shmget()创建共享内存
int shmget(key_t key, size_t size, int shmflg);
入参:
(1).由ftok生成的key标识,标识系统的唯一IPC资源
(2).需要申请共享内存的大小
(3).访问权限(和文件权限是一致的)
如果共享内存不存在,则需要使用IPC_CREAT或IPC_EXCL创建共相册i村
如果共享内存已存在,可以使用IPC_CREAT或直接传0,获得共享内存
返回值:
成功则返回一个新建或者已存在的共享内存标识符,取决于shmflg的参数
失败返回-1
(4)shmat()挂接共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
入参:
(1).共享存储段的标识符
(2).shmaddr=NULL,则存储段连接到由内核选择的第一个可以挂接的地址上(推荐使用)
(3).指定SHM_RDONLY,则以只读方式连接此段,0是可以以读写方式连接此段
返回值:
成功返回共享存储段的指针(虚拟地址)
失败返回-1
(5)shmdt()去关联共享内存
当一个进程不需要共享内存的时候,就需要去关联,该函数并不删除指定的内存区,而是将之前用shmat函数连接好的共享内存区脱离目前的进程
int shmdt(const void *shmaddr);
入参:
连接以后返回的地址
返回值:
成功返回0
失败返回-1
(6)shmct()销毁共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
入参:
(1).共享存储段标识符
(2).指定的执行操作,设置为IPC_RMID时表示可以删除共享你内存
(3).buf:设置NULL即可
返回值:
成功返回0
失败返回-1
例子:
(1) fork两个子进程,server和client, 进行通信;
(2) server 端建立一个key为75的共享区,并将第一个字节置为-1,作为数据空的标志。等待其他进程发 来的消息。当该字节的值发生变化时,表示收到了信息,进行处理。然后再次把它的值设为-1,如果遇到的值为0,则视为为结束信号,取消该队列,并退出server。 server 每接收到一次数据后显 示"(server)received”。
(3) client 端建立一个key为75的共享区,当共享取得第一个字节为-1时,server端空闲,2可发送请求。 client 随即填入9到0。期间等待sserver端的再次空闲。进行完这些操作后,client退出。client每发送一次 数据后显示"(client)sent"。 (4)父进程在server和client 均退出后结束。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/shm.h>
#include<sys/ipc.h>
#define SHMKEY 75
void server();
void client();
int shmid, i;
int *addr = NULL;
int main()
{
while((i = fork()) == -1);
if(!i)
{
server();
}
system("ipcs -m");
while((i = fork()) == -1);
if(!i)
{
client();
}
wait(NULL);
wait(NULL);
system("ipcs -m");
return 0;
}
void server()
{
shmid = shmget(SHMKEY, 1024, 0777 | IPC_CREAT);
if(shmid == -1)
{
perror("Error");
exit(1);
}
printf("server() shmid = %d\n", shmid);
addr = (int *)shmat(shmid, NULL, 0);
do
{
*addr = -1;
while(*addr == -1);
printf("(server) received addr的内容是 = %d\n", *addr);
}while(*addr);
shmctl(shmid, IPC_RMID, 0);
exit(0);
}
void client()
{
shmid = shmget(SHMKEY, 1024, 0777);
if(shmid == -1)
{
perror("Error");
exit(1);
}
printf("client() shmid = %d\n", shmid);
addr = (int *)shmat(shmid, NULL, 0);
for(int i = 9; i >= 0; i--)
{
while(*addr != -1);
printf("(client) server save *addr = %d\n", *addr);
*addr = i;
printf("(client) sent *addr = %d\n", i);
}
exit(0);
}
2. 进程组和会话
2.1 概念和特性
进程组,每个进程组有一个领头的进程。进程组是一个或多个进程的集合,通常他们与一组作业相关联,可以接受来自同一个终端的各种信号
每个进程组都有唯一的进程组ID(整数,也可以存放在pid_t类型中)
进程组由进程组ID来唯一标识,除了进程号(PID)外,进程组ID也是一个进程的必备属性之一
#include<unistd.h>
pid_t getpgrp(void);
返回值是进程的进程组ID
2.2 创建会话
创建一个会话需要注意一下6点注意事项:
1. 调用进程不能是进程组组长,该进程会变成新会话的首进程
2. 该进程成为一个新进程组的组长进程
3. 需要有root权限(ubuntu中是不需要的)
4. 新会话丢弃原有的控制终端,该会话没有控制终端
5. 该调用进程是组长进程,则出错返回
6. 建立新会话时,先调用fork,父进程终止,子进程调用setsidu
2.3 getsid函数
这会获得进程所属会话ID
pid_t getsid(pid_t pid);
返回值:
成功:返回调用进程的会话ID
失败:-1,设置error
入参:
pid为0表示查看当前进程session ID
ps ajx
查看系统中进程的命令
参数a表示不仅当前用户的进程,也列出所有其他用户的进程
参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程
参数j表示列出与作业息息相关的信息,小组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程
2.4 setsid函数
创建一个会话,并且以自己的ID设置进程组ID,同时也是新会话的ID
pid_t setsid(void);
返回值:
成功:返回调用进程的会话ID
失败:-1,设置errno
调用了setsid函数的进程,既是新的会长,也是新的组长
例子: fork一个子进程,并使其创建一个新会话。 查看进程组ID、会话ID前后变化
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main()
{
pid_t pid;
if((pid=fork())<0)
{
sys_err("fork err");
}
else if(pid==0)
{
printf("child process PID is %d\n" ,getpid());
printf("Group ID of child is %d\n", getpgid(0));
printf("Session ID of child is %d\n",getsid(0));
sleep(10);
//子进程非组长进程,故其成为新会话首进程,且成为组长进程。该进程组id即为会话>
setsid();
printf( "Changed:\n" );
printf("child process PID is %d\n", getpid());
printf("Group ID of child is %d\n",getpgid(0));
printf("Session ID of child is %d\n", getsid(0));
sleep(20);
exit(0);
}
return 0;
}
2.5 系统守护进程介绍和创建
守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务,很多守护进程在系统引导的时候启动,并且一直运行到系统关闭,另一些只在需要的时候才启动,完成任务以后会自动结束
2.5.1 守护进程简介
守护进程是一个在后台运行并且 不受任何终端控制的进程 。 Unix 操作系统有很多典型的守护进程(其数目根据需要或20~50 不等),它们在后台运行,执行不同的管理任务。
2.5.1 创建守护进程步骤
1. 创建子进程, 父进程退出
所有工作在子进程中进行形式上脱离了控制終端
2. 在子进程中创建新会话
setsid)函数
使子进程完全独立出来,脱离控制
3. 改变当前目录(比如为根目录)
chdir()函数
防止占用可卸载的文件系统
也可以换成其它路径
4.重设文件权限掩码
umask()函数
防止继承的文件创建屏蔽宇拒绝某些权限
增加守护进程灵活性
5.关闭文件描述符。
继承的打开文件不会用到,浪费系统资源,无法卸载
6. 开始执行守护进程核心工作守护进程退出处理程序模型
例:创建一个守护进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main()
{
pid_t pid;
int ret,fd;
pid = fork();
if (pid == -1)
{
sys_err("setsid error");
}
else if (pid > 0)
{
exit(0);//父进程终止
}
else
{
pid = setsid();//创建新会话
if (pid == -1)
{
sys_err("setsid error" );
}
ret=chdir("/home/xw/");//改变工作目录位置,保证目录有效
if(ret==-1)
{
sys_err("chdir err");
}
umask(0022);//改变文件访问权限掩码
close(STDIN_FILENO);//关闭文件描述符 0
fd=open("/dev/null",O_RDWR); //fd ---> 0
if(fd==-1)
{
sys_err("open err");
}
dup2(fd,STDOUT_FILENO);//重定向
dup2(fd,STDERR_FILENO);//重定向
while(1); //模拟守护进程业务逻辑需求
}
return 0;
}
3. 信号机制
3.1 信号概念
现代战争中的信号弹
1. 简单
2. 不能携带大量信息
3. 满足某个特设条件才发送。
信号是信息的载体, Linux/UNIX 环境下,古老、经典的通信方式,现下依然是主要的通信手段。
Unix 早期版本就提供了信号机制,但不可靠,信号可能丢失。 Berkeley 和 AT&T 都对信号模型做了
更改,增加了可靠信号机制。但彼此不兼容。 POSIX.1 对可靠信号例程进行了标准化
3.2.1 信号机制
信号机制是一种使用信号来进行进程之间传递消息的方法,信号的全称为 == 软中断信号 == ,简称软中断。信号的本质是软件层次上对中断的一种模拟(软中断)。它是一种异步通信的处理机制,事实上,进程并不知道信号何时到来。
比如: A 给 B 发送信号, B 收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。
信号的特质 : 由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责产生和发送的,由内核处理。
信号是软件层面上的 “ 中断 ” 。一旦信号产生 , 无论程序执行到什么位置,必须立即停止运行,处理信号,
处理结束,再继续执行后续指令。
3.2 与信号相关的事件和状态
产生信号 :
1.按键产生 ,当用户按下某些中断按键后引发某些终端产生信号,如 : Ctrl+c (终止进程)、 Ctrlt+z (暂停进程到后台)
2.系统调用产生,如 : kill 、 raise 、 abort ,可以给一个进程或进程组发送一个信号,此时发送和接收信号的进程或者进程组的所有者必须相同。
3.软件条件产生 ,如 : 定时器 alarm (比如 sleep 函数)
4.硬件异常产生,如 : 非法访问内存 ( 段错误 ) 、除 0( 浮点数例外 ) 、内存对齐出错 ( 总线错误 ). 这种异常信号通常会由硬件检测并通知linux 内核,然后内核产生信号发送给相关进程
5.命令产生 ,如 : 用户调用 kill 命令将信号发送给其他进程
递达
递送并且到达进程,由内核处理掉
未决
产生和递达之间的状态。主要由于阻塞 ( 屏蔽 ) 导致该状态。
信号的处理方式:
Linux 的每一个信号都有一个缺省的动作,典型的缺省动作是终止进程,当一个信号到来的时候收到这个信号的进程会根据信号的具体情况提供以下三种不同的处理方式:
(1) 捕捉: 对于需要处理的信号,由用户的函数来处理。
(2) 忽略: 丢弃某个信号,对该信号不做任何处理。
(3) 按系统的默认值处理 ,这种缺省操作大多数使得进程终止,进程通过系统调用 signal 函数来指定进程对某个信号的处理行为。
Linux 内核的进程控制块 PCB 是一个结构体, task struct, 除了包含进程 pid ,工作目录,用户 id, 组 id, 文件描述符表,还包含了信号相关的信息,主要指“阻塞信号集”和 “ 未决信号集 ”。
阻塞信号集 ( 信号屏蔽字 ):
将某些信号加入集合,对他们设置屏蔽(对应位设置为 1 ),当屏蔽 x 信号后,再收到该信号,该信号属于未决信号集,直到解除屏蔽后才能处理。
未决信号集 :
1. 动作:信号产生后,未决信号集中描述该信号的位立刻翻转为 1 ,表信号处于未决状态。当信号被处理对应位翻转回为0 。这一时刻往往非常短暂。
2. 信号产生后由于某些原因 ( 主要是阻塞 ) 不能抵达。这类信号的集合称之为 未决信号集 。在屏蔽解除前,信号一直处于未决状态。
3.3 信号编号
在头文件中定义了64种信号,这些信号的名字都以SIG开头,且都被定义为正整数,称为信号 编号。
查看所有信号===>使用命令:kill -l
不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64 称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不同。 普通信号:场景应用程序开发使用,信号都有默认处理动作, 实时信号:场景底层驱动开发使用,信号处理可以由用户处理
3.4 信号四要素
与变量三要素类似的,每个信号也有其必备四要素,分别是:
1.编号
2.名称
3.事件
4.默认处理动作
使用信号前:应先确认其四要素,再使用
可通过 man 7 signal 查看帮助文档获取。也可查看 /usr/include/signal.h
在标准信号中,有一些信号是有三个 “Value", 第一个值通常对 alpha 和 sparc 架构有效,中间值针对
x86 、 arm 和其他架构,最后一个应用于 mips 架构。 一个 ’_‘ 表示在对应架构上尚未定义该信号。
默认动作 :
Term:终止进程。
lgn:忽略信号 ( 默认即时对该种信号忽略操作 ) 。
Core:终止进程,生成 Core 文件。 ( 查验进程死亡原因,用于 gdb 调试 ) 。
Stop:停止 ( 暂停 ) 进程。
Cont:继续运行进程。
注意:从 man 7 signal 帮助文档中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
这里特别强调了 9) SIGKILL 和 19) SIGSTOP 信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将 其设置为阻塞。
4. 信号的产生
4.1 终端按键产生信号
Ctrl+c → 2) SIGINT ( 终止 / 中断 ) "INT" --interrupt
Ctrl+z → 20) SIGTSTP ( 暂停 / 停止 ) "T" --terminal 终端
Ctrl+\ → 3) SIGQUIT ( 退出 )
4.2 终端异常产生信号
除 0 操作 → 8) SIGFPE ( 浮点数例外 ) "P"---- float 浮点数。
非法访问内存 → 11) SIGSEGV ( 段错误 ) 。
总线错误 → 7) SIGBUS
例:除0操作
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int a=10;
int b= a/0;
return 0;
}
4.3 Kill函数,命令产生信号
kill 命令产生信号 : kill 所要杀的进程pid 信号名称
kill 函数 : 给指定进程发送指定信号 ( 不一定杀死 ) 。
int kill(pid_t pid, int sig)
返回值:
成功: 0;
失败: -1 (ID非法,信号非法,普通用户杀 init 进程等权级问题 ) ,设置 errno.
sig: 不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致
pid>0: 发送信号给指定的进程
pid=0: 发送信号给与调用 kill 函数进程属于同一进程组的所有进程
pid<0: 取 |pid| 发给对应进程组
pid=-1: 发送给有权限发送的系统中所有进程
权限保护 :
super 用户 (root) 可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。 kill -9 (root 用户的pid)是不可以的。 同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。
普通用户基本规则是 : 发送者实际或有效用户 ID== 接收者实际或有效用户 ID 。
例程:父进程发送信息给子进程结束标志
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
pid_t pid = fork();
if(pid>0)
{
printf("parent,pid = %d\n", getpid());
while(1);
}
else if(pid==0)
{
printf("child pid = %d, ppid = %d\n", getpid(), getppid());
sleep(2);
kill(getppid(), SIGKILL);
}
return 0;
}
4.4 软件信号的产生
alarm 函数
设置定时器 ( 闹钟 ) 。在指定 seconds 后,内核会给当前进程发送 14) SIGALRM 信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一个定时器。
unsigned int alarm ( unsigned int seconds );
参数:定时秒数
返回值:上次给定的剩余时间,无错误现象
常用 : 取消定时器 alarm(0), 返回旧闹钟余下秒数。
例 : alarm(5) → 3sec → alarm(4) → 5sec → alarm(5) → alarm(0) 。
定时,与进程状态无关 ( 自然定时法 )! 就绪、运行、挂起 ( 阻塞、暂停 ) 、 终止、僵尸 ... 无论进程处于何种状态alarm 都计时。也称为 自然时间计时法 。
取消闹钟 : alarm(0)
练习 : 编写程序,测试你使用的计算机 1 秒钟能数多少个数。 [alarm.c]
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int i;
unsigned int ret =alarm(1);
// printf("ret01 =%d\n",ret);
// sleep(1); // ret =alarm(2);
// printf("ret02 =%d\n",ret);
for(i = 0; ; i++)
{
printf("%d\n", i);
}
return 0;
}
优化瓶颈
使用 time 命令查看程序执行的时间。
程序运行的瓶颈在于 IO ,优化程序,首选优化 IO 。
实际执行时间 = 系统时间 + 用户时间 + 等待时间。