------------------------本文为学习进程记录的学习笔记,如有问题欢迎指正 --------------------------
目录
1.定义
狭义定义:进程是正在运行的程序的实例
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
2.进程的种类
1.用户进程:也称为应用进程,是由用户启动的进程来执行应用程序。用户进程是在用户空间中运行的,并且可以与其他用户进程进行通信。
2.系统进程:也称为内核进程,是由操作系统内核启动和管理的进程。
3.守护进程:是在后台运行的进程,独立于用户会话的一种特殊进程。守护进程通常在系统启动时启动,并持续运行,提供系统服务或执行特定的任务。
4.孤儿进程:当父进程意外终止或退出时,子进程可能成为孤儿进程。孤儿进程将由操作系统接管,并由操作系统中的一个特殊进程(通常是init进程)接收和回收。
5.僵尸进程:当子进程完成执行(终止)后,但父进程尚未调用wait()或waitpid()来获取子进程的退出状态码时,子进程将成为僵尸进程。僵尸进程仍保留在进程表中,但不再执行任何任务。
2.进程的内存布局
![](media/1.进程空间分配.png)
各个分段的含义:
-
文本段(Text):也称为代码段。进程启动时会将程序的代码加载到物理内存中,文本段映射到这片物理内存。
-
数据段(Data):包含程序显式初始化的全局变量和静态变量,即已初始化且初值不为0的全局变量(也包括静态全局变量)和静态局部变量
-
未初始化数据(BSS):未初始化的全局变量和静态变量
-
栈(Stack):存储局部、临时变量,函数调用时,存储函数的返回指针,用于控制函数的调用和返回。在程序块开始时自动分配内存,结束时自动释放内存,其操作方式类似于数据结构中的栈。
-
堆(Heap):存储动态内存分配,需要程序员手工分配,手工释放.注意它与数据结构中的堆是两回事,分配方式类似于链表。
栈的内存地址向下增长,堆得内存地址向上增长。
-
内存映射段(Memory Mapping):在栈与堆之间,有一个内存映射端。内核通过这一段将文件的内容直接映射到内存,进程可以通过 mmap 系统调用请求这种映射。内存映射是一种方便高效的文件 I/O 方式,所以它也被用来加载动态库。
-
内核段(Kernel):这部分是操作系统内核运行时所占用内存在各进程虚拟地址空间中的映射。所有进程都有,且映射地址相同,因为都映射到内核使用的内存。这段内存只有内核能访问,用户进程无法访问到该段落。
3.进程控制块(PCB)
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息。Linux内核的进程控制块是task_struct结构体。
struct task_struct
{
volatile long state; //说明了该进程是否可以执行,还是可中断等信息
unsigned long flags; // flags 是进程号,在调用fork()时给出
int sigpending; // 进程上是否有待处理的信号
mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同 //0-0xBFFFFFFF for user-thead //0-0xFFFFFFFF for kernel-thread
//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
volatile long need_resched;
int lock_depth; //锁深度
long nice; //进程的基本时间片
//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER
unsigned long policy;
struct mm_struct *mm; //进程内存管理信息
int processor;
//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新
unsigned long cpus_runnable, cpus_allowed;
struct list_head run_list; //指向运行队列的指针
unsigned long sleep_time; //进程的睡眠时间
//用于将系统中所有的进程连成一个双向循环链表, 其根是init_task
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_head local_pages; //指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal; //父进程终止时向子进程发送的信号
unsigned long personality;
//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
int did_exec:1;
pid_t pid; //进程标识符,用来代表一个进程
pid_t pgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_t session; //进程的会话标识
pid_t tgid;
int leader; //表示进程是否为会话主管
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; //线程链表
struct task_struct *pidhash_next; //用于将进程链入HASH表
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; //供wait4()使用
struct completion *vfork_done; //供vfork() 使用
unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
//it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value
//设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据
//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。
//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送
//信号SIGPROF,并根据it_prof_incr重置时间.
//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种
//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据
//it_virt_incr重置初值。
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_list real_timer; //指向实时定时器的指针
struct tms times; //记录进程消耗的时间
unsigned long start_time; //进程创建的时间
//记录进程在每个CPU上所消耗的用户态时间和核心态时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
//内存缺页和交换信息:
//min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换
//设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。
//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid
//euid,egid为有效uid,gid
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件
//系统的访问权限时使用他们。
//suid,sgid为备份uid,gid
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; //记录进程在多少个用户组中
gid_t groups[NGROUPS]; //记录进程所在的组
//进程的权能,分别是有效位集合,继承位集合,允许位集合
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
unsigned short used_math; //是否使用FPU
char comm[16]; //进程正在运行的可执行文件名
int link_count, total_link_ count; //文件系统信息
//NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空
struct tty_struct *tty;
unsigned int locks;
//进程间通信信息
struct sem_undo *semundo; //进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
//进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct thread_struct thread;
struct fs_struct *fs; //文件系统信息
struct files_struct *files; //打开文件信息
spinlock_t sigmask_lock; //信号处理函数
struct signal_struct *sig; //信号处理函数
sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位
struct sigpending pending; //进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock;
void *journal_info;
};
4.进程源语
fork() #进程创建
exec() #进程功能重载
wait() #进程回收
waitpid() #进程回收,wait函数的升级版
fork()
开发者调用fork后,内核创建子进程,而后使用读时共享写时复制的方式,解决继承困境。
读共享采用映射技术,将父进程的用户空间映射给子进程,子进程通过映射可以读访问父进程的资源,写的时候采用clone技术.
写时复制
在写时复制中,当多个进程或线程需要访问相同的数据时,不会立即进行数据的复制,而是共享同一份数据副本。只有当某个进程或线程试图修改数据时,才会进行数据的复制,以确保修改操作不会影响其他进程或线程的访问。
具体来说,当多个进程或线程共享同一份数据时,它们实际上共享的是指向数据副本的指针。当某个进程或线程试图修改数据时,操作系统会在内存中为该进程或线程创建一个新的副本,然后将修改操作应用于新的副本,而不会影响其他进程或线程。这种延迟复制的方式可以节省内存和时间开销,特别适用于需要频繁复制数据的场景。
写时复制技术常用于操作系统的进程管理和文件系统中,以提高系统的性能和资源利用率。例如,当一个进程创建子进程时,子进程会与父进程共享相同的内存空间,只有在需要修改数据时才会进行复制。这样可以避免不必要的数据复制,提高进程的创建和执行效率。
写时复制只适用于读多写少的场景。如果多个进程或线程频繁地修改数据,写时复制的效果可能会受到影响,因为频繁的数据复制操作会增加系统的开销。
exec()
exec系列函数(execl、execlp、execle、execv、execvp)
以上exec系列函数区别:
1,带l 的exec函数:execl,execlp,execle,表示后边的参数以可变参数的形式给出且都以一个空指针结束。
2,带 p 的exec函数:execlp,execvp,表示第一个参数path不用输入完整路径,只有给出命令名即可,它会在环境变量PATH当中查找命令
3,不带 l 的exec函数:execv,execvp表示命令所需的参数以char *arg[]形式给出且arg最后一个元素必须是NULL
4,带 e 的exec函数:execle表示,将环境变量传递给需要替换的进程
execl函数
用这个函数可以把当前进程替换为一个新进程,且新进程与原进程有相同的PID。
-
execl可以以极小的代价,使用系统现有的命令或程序功能
-
如果重载服务器,可以避免服务器过于臃肿,插件模块的更新与迭代也与服务器无关,不会影响线上服务器
wait() #进程回收
wait()阻塞函数,子进程存在,但未产生僵尸,wait阻塞等待,产生僵尸后立即回收。
注意:wait每调用一次只能回收一次
僵尸进程——如果进程结束没有被及时回收,则产生僵尸进程,僵尸进程是内存泄漏
waitpid()函数
支持非阻塞回收,无需使父进程持续陷入阻塞,可以让回收与任务之间切换,交替执行
pid_t pid=waitpid(pid_t pid,int * status,int flag);
第一个参数pid:
-
-1 回收任意子进程,
-
>0 传递一个指定进程的id,回收指定进程
-
0 表示回收同组的任意子进程
-
<-1 指定组id,跨组回收(回收父子关系的进程)
5.僵尸进程
僵尸进程是指一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。
僵尸进程产生原因
子进程先结束,父进程需回收子进程(PCB),否则会造成内存泄漏,当子进程结束时,如(exit),系统会回收子进程用户区和部分内核区(不回收PCB),遗留下来的PCB若不回收会造成内存泄漏。
僵尸进程危害
-
内存泄漏(持续占用系统资源,无法重新分配),PCB为较为庞大的结构体,很多内部成员占据着较大的内存
-
影响子进程的创建(系统创建子进程的数量为可以创建的PCB的数量,僵尸进程大量占有PCB,则可新创建的子进程会减少)
如何回收僵尸进程
只能通过父进程回收
-
阻塞方式:pid_t wait(int* status)函数,调用完wait后等待子进程结束立即回收,调用一次只回收一个僵尸进程(PCB),进程PCB会将退出状态通过参数返回,便于父进程查看子进程结束的原因;回收成功返回子进程的PID,如果没有子进程或回收失败则返回-1。
-
非阻塞方式:pid_t waitpid(pid_t pid, int *status, int option)函数,采用轮询的方式回收子进程。
pid:
pid > 0:回收指定PID的子进程
pid = 0:回收和父进程同组的子进程
pid = -1:回收所有子进程
option:WNOHANG,设置为非阻塞回收。
返回值:
-1:回收失败(没有子进程)
0:非阻塞返回(暂时不需要回收)
大于0:返回子进程的PID
3.设计父子进程通信机制,子进程结束时向父进程发送信号,父进程接收到信号后回收子进程资源。
6.孤儿进程 Orphan
-
孤儿进程属于后台程序,使用终端快捷键对后台程序无效,只能通过kill命令利用信号杀死进程
-
孤儿进程的危害是弹性的,取决于孤儿进程执行的任务
-
如果孤儿进程持续占用系统资源,会影响操作系统的稳定性
-
即使孤儿进程挂起或睡眠,也要尽快杀死孤儿进程,因为它会占用pcb,也有系统开销,影响进程创建
孤儿进程产生原因
父进程先于子进程结束,子进程成为孤儿进程,持续占用系统资源,失控给系统造成危害。
Linux下所有孤儿进程都会由托管进程进行回收。父进程结束,子进程变为孤儿进程,父子关系变更,子进程成为托管进程的子进程。
孤儿进程危害
1.内存泄露影响新进程创建,孤儿进程还在执行,可能持续占用系统资源
2.孤儿进程的危害是有弹性的,如果孤儿进程阻塞或挂起为常态,损失很小,但如果孤儿进程是持续运行的,并且大量申请系统资源,那么危害较大
孤儿进程预防
-
在创建子进程后,父进程在子进程执行完毕前不退出,可以使用系统调用
wait()
或waitpid()
等待子进程结束。 -
使用守护进程(Daemon Process):将子进程设置为守护进程,当父进程退出后,守护进程会被init进程接管并自动回收资源,避免产生孤儿进程。
孤儿进程的检测与处理
创建一个进程和写端建立管道,p向L发送自己的pid,如果P是正常退出即回收完全部子进程后退出就向L发送“quit”,然后正常退出,否则L如果发现P失效了就说明L是异常退出,就按组杀死进程。
7.Deamon守护进程
-
守护进程又名精灵进程,是操作系统经典的后台服务程序,后台进程不干涉前台,在后台持续工作,守护进程在后台都是低消耗模式(进程属于睡眠态S)
-
守护进程工作模式:间隔执行,定时执行,条件触发执行
-
守护进程生命周期更长,开机启动关机才结束,生命周期随系统持续
-
系统守护进程,是操作系统组件,每个组件都负责一类服务,维护系统正常执行,维护系统功能稳定
-
严格意义上来讲,守护进程(后台程序)就是孤儿进程,人为产生孤儿进程,不仅无害还可以为系统服务,为软件服务
9.进程间通信方法
进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。
进程间通信的应用场景
数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
管道
管道是进程间通信的一种方式,可以把管道看作是一个文件,由操作系统内核进行管理。
管道分为命名管道和匿名管道,命名管道是一个存在于文件系统中的一个文件,通过mkfifo来创建,可以在不同的进程间传递信息。
匿名管道相当于一个字节流,用于有亲缘关系的进程间通信。数据传输是单向的,一端写一端读。
管道的基本特性:
1.方向性(数据流通方向)
2.存储性(缓存少量数据,暂时存储)
3.确定通信方向(单工)
PIPE匿名管道
匿名管道通过打开的文件描述符来标识的。——用于具有亲缘关系间进程之间的通信。
进程的内核空间是共享内存,绝大多数进程通信方式都是在内核层完成的。
用户层将数据传到内核层,接收方就能从内核层找到要接收的数据,再传给用户层就完成了进程间的通信。
pipe()创建管道函数,会返回两个描述符,用来读写管道,接收方如果和发送方共用一个pipe就能共用一个管道了,可以通过fork父子进程实现。
int pipe(int fds[2]) #可以创建一个管道,创建成功后传出管道的使用描述符
fds[0] #管道读描述符,用于读取管道缓冲区的数据队列出队操作
fds[1] #管道写描述符,用于向管道写数据队列入队操作
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#define MSG "Hi"
//父进程利用匿名管道通信
int main(void)
{
pid_t pid;
int fds[2];
pipe(fds);//管道创建成功,传出描述符
pid=fork();
if(pid>0){
close(fds[0]);
//向管道写入数据
write(fds[1],MSG,strlen(MSG));
printf("parent %d Send msg success..\n",getpid());
close(fds[1]);
wait(NULL);
}else if(pid==0){
close(fds[1]);
int len;
char buffer[1500];
bzero(buffer,sizeof(buffer));
len=read(fds[0],buffer,sizeof(buffer));
printf("child %d recv pipe ,msg=%s\n",getpid(),buffer);
close(fds[0]);
exit(0);
}else{
perror("fork call failed");
exit(0);
}
return 0;
管道的销毁和释放
管道的销毁释放,当管道的引用计数为0,系统会自行释放管道空间
匿名管道的优缺点
优点: 经典的进程间通信手段,实现使用方便
缺点:只能具有亲缘关系的进程完成数据交互(独立的进程无法使用匿名管道通信)
默认情况下匿名管道用于传输无格式字节流(接收方不清楚数据的类型和大小) ,可以自定义数据格式
匿名管道为单工通信
单工:任意时刻,非读即写
半双工:任意时刻,非读即写,不同时刻方向可切换(单工的一种)
全双工:任意时刻,可以同时读写
匿名管道使用时的几种特殊情况(具有普遍意义)
1.写阻塞,读写两端有效,读端未读数据,写端写管道,当写满管道后,产生写阻塞
2.读阻塞,读写两端有效,写端未向管道写数据,读端读取空管道时,产生读阻塞
3.写端失效,读端读取管道剩余内容后,再次读返回0
4.读端失效,写端尝试向管道写数据,系统会向写端进程发送SIGPIPE信号,杀死写端进程
FIFO命名管道
命名管道是一种有名的通信方式,可以实现无关进程之间的通信。它可以在不具有亲缘关系的进程之间传递数据,并且可以实现双向通信。
创建的两种方式:
1.mkfifo names #命令方式
2.mkfifo(const char* names,int mod) #函数方式
#创建有名管道成功后,都会生成一个同名的管道文件,(在内核里也会有一块缓冲区,文件指针指向缓冲区)文件类型p
#使用有名管道可以完成进程通信,但是没有任何限制(是否亲缘都可以)
匿名管道的特殊情况适用于命名管道
- 命名管道文件的访问权限:管道文件的使用规则,必须同时满足读写两种权限才可以访问管道文件,如果只有其中一种权限,那么open 阻塞,等待另外一种。命名管道访问与权限有关,与进程数无关,一个进程拥有足够的权限,一样可以访问管道文件。
- 一个进程中有多个读序列,阻塞只对第一个读序列生效,其他所有读序列被设置为非阻塞
-
原子访问与非原子访问:
- 原子访问(一次写入的数据小于等于管道的大小)
- 非原子访问(一次写入的数据大于管道的大小)
写端代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MSG "Hi Can u hear me?"
//写端
int main(void)
{
int wfd;
wfd=open("FIFO_FILE",O_WRONLY);
write(wfd,MSG,strlen(MSG));
printf("write pro %d send success..\n",getpid());
close(wfd);
return 0;
}
读端代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//读端
int main(void)
{
int rfd;
int len;
char buf[1500];
bzero(buf,sizeof(buf));
rfd=open("FIFO_FILE",O_RDONLY);
len=read(rfd,buf,strlen(buf));
printf("read pro %d read msg %s\n",getpid(),buf);
close(rfd);
return 0;
}
匿名管道和命名管道的区别?
对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。
对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。
匿名管道:
a.读端关闭->如果写端继续写,则会收到系统发送的信号,写端被关闭。
b.读端存在->如果始终未读取数据,写端可以继续写入数据,写满后,写阻塞
c.写端关闭->如果读端读取剩余数据,可以。否则返回0。
d.写端存在->始终未写入数据,读阻塞。
命名管道:
a. 使用时,如果多个读端,则只有第一个读端阻塞读取,其他非阻塞读取。
b. 阻塞读取时,如果写入数据n > 管道缓存区 -> 写入的n字节是非原子的,直到n字节全部写完,write才返回。如果写入数据n <= 管道缓存区 -> 写入的n字节是原子的,如果没有n字节空间,则写阻塞
c. 非阻塞读取时,如果写入数据n > 管道缓存区 -> 如果管道满,返回-1,并设置EAGAIN,否则会随机写入1-n字节数据,具体写多少需要用户查看write返回值。
如果写入数据n <= 管道缓存区 -> 如果空间足够,则可以写入,否则返回-1
MMAP 文件映射
mmap用途比较多样,可以通过映射的方式处理文件,也适合处理大文件,可以进行进程间的通信,进行零拷贝数据传输的一种方式。
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存。
分类
1、文件映射 磁盘文件映射进程的虚拟地址空间,使用文件内容初始化物理内存。
2、匿名映射 初始化全为0的内存空间。
而对于映射关系是否共享又分为
1、私有映射(MAP_PRIVATE) 多进程间数据共享,修改不反应到磁盘实际文件,是一个copy-on-write(写时复制)的映射方式。
2、共享映射(MAP_SHARED) 多进程间数据共享,修改反应到磁盘实际文件中。
所以进程间的通信可以让两个进程对同一个映射文件进行共享映射。
映射文件大小不能为空,要用\0填充,拓展空文件。
映射文件大小要小于等于共享文件大小,否则权限越界。
函数原型
1.void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
成功执行时,如果mmap 调用成功返回映射内存的地址 void *p
如果映射失败返回map_failed,进行错误处理获取失败原因。
2.munmap(void* p,int size);映射内存使用完毕后,通过该函数释放映射内存
写端代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
typedef struct
{
int price;
char Gname[1024];
}msg_t;
int main(void)
{
//创建映射文件
int fd;
int No=1;
if((fd=open("MAP_IPC",O_RDWR|O_CREAT,0664))==-1){
perror("open call failed");
exit(0);
}
//拓展文件
ftruncate(fd,sizeof(msg_t));
msg_t* ptr=NULL;
if((ptr=mmap(NULL,sizeof(msg_t),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0))==MAP_FAILED){
perror("mmap call failed");
exit(0);
}
close(fd);
ptr->price=89;
bzero(ptr->Gname,1024);
while(1){
//修改映射内存数据
sprintf(ptr->Gname,"Apex %d",No++);
sleep(1);
}
munmap(ptr,sizeof(msg_t));
return 0;
}
读端代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
typedef struct
{
int price;
char Gname[1024];
}msg_t;
int main(void)
{
//创建映射文件
int fd;
int No=1;
if((fd=open("MAP_IPC",O_RDWR|O_CREAT,0664))==-1){
perror("open call failed");
exit(0);
}
//拓展文件
msg_t* ptr=NULL;
if((ptr=mmap(NULL,sizeof(msg_t),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0))==MAP_FAILED){
perror("mmap call failed");
exit(0);
}
close(fd);
while(1){
//显示结果
printf("Gname %s ,Price %d \n",ptr->Gname,ptr->price);
sleep(1);
}
munmap(ptr,sizeof(msg_t));
return 0;
}
mmap的权限问题
如果用户通过共享映射来读写文件,那么即使没有文件权限,也可以访问文件了,为了避免这种情况,mmap会查看open中使用的权限,如果是只读打开,就不允许读写权限映射。
read与mmap的区别
从图中可以看出,mmap要比普通的read系统调用少一次copy,因为read调用,进程是无法直接访问kernel space 的,所以在read系统调用返回前,内核需要将数据从内核复制到进程指定的buffer,但mmap之后,进程可以访问mmap的数据。
优缺点
优点如下:
1、对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。
2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。
4、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。
缺点如下:
1.文件如果很小,是小于4096字节的,比如10字节,由于内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。虽然被映射的文件只有10字节,但是对应到进程虚拟地址区域的大小需要满足整页大小,因此mmap函数执行后,实际映射到虚拟内存区域的是4096个字节,11~4096的字节部分用零填充。因此如果连续mmap小文件,会浪费内存空间。
2.对变长文件不适合,文件无法完成拓展,因为mmap到内存的时候,你所能够操作的范围就确定了。
3.如果更新文件的操作很多,会触发大量的脏页回写及由此引发的随机IO上。所以在随机写很多的情况下,mmap方式在效率上不一定会比带缓冲区的一般写快。
信号
信号是一种软件中断,用于通知进程发生了某个事件。它可以用于进程间的通信和同步。
使用信号技术完成父子进程间通信,实现父子进程交替报数,数据要相互传递。
思路:
1.父子进程均要完成捕捉设定,让信号失效
2.使用开发者信号,SIGUSR1(10),SIGUSR2(12)
3.sigqueue(pid_t pid,int signo,union sigval val);//发送信号的同时携带数据,发送的数据可以让捕捉函数的参数捕捉,捕捉函数中直接用就可以
4.通过继承的特性让父进程设置信号屏蔽,子进程继承此屏蔽,而后完成捕捉设定,解除屏蔽(避免在子进程刚刚被创建还没来得及捕捉就有信号递达而被杀死)
信号量
消息队列
套接字
它是更为通用的进程间通信机制,可用于不同机器之间的进程间通信。
总结
以下是这些进程间通信方式各自的一些优缺点:
管道:
优点:简单易用。
缺点:只能用于有亲缘关系的进程,效率相对较低,不适合大量数据传输。
命名管道:
优点:可用于无亲缘关系的进程间通信。
缺点:依然存在一定局限性。
消息队列:
优点:可以实现异步通信,能容纳多条消息。
缺点:消息大小有限制,消息队列满时可能会阻塞。
信号量:
优点:能很好地实现进程同步。
缺点:主要用于同步控制,数据通信能力相对较弱。
共享内存:
优点:效率非常高,数据传输速度快。
缺点:需要配合同步机制来避免数据竞争和一致性问题。
套接字:
优点:可跨网络,适用范围广。
缺点:实现相对复杂一些。