文章介绍了多进程相关概念及管理方法。程序使静态指令集合,进程使运行中的程序实例。Linux通过进程表管理进程,使用ps命令查看进程信息。创建进程可通过system函数或exec族函数,后者更加高效。进程间的通信包括管道、消息队列、信号、共享内存和信号量,各有优缺点,适用于不同的场景。
进程
什么是程序
计算机程序是由计算机执行某一特定任务的指令(最基础的计算机动作称为指令)的集合。
程序是众多多编程语言写出来的文本,这些文本可以让计算机做指定的事;例如exe文件,这就是一个编译完成的程序。
什么是进程
一个程序,它被存放在硬盘上,是静态的;但一个程序启动后,程序被操作系统装载到内存并分配一定的资源,此时就会在内存里产生一个进程,再次启动又会产生一个进程。
进程相对于程序就是一个动态的概念。进程是曹祖系统进行资源分配的基本单位。
简单理解:一个程序运行对应着一个进程。
linux允许多个用户同时访问系统,每个用户可以同时运行多个程序,这里就需要用到多个程序。
管理进程
进程表
linux会提供一个“进程表”,把当前加载在内存中的所有进程相关信息都保存在表中,包括PID、进程状态、命令字符串等等。
• 操作系统通过进程的“进程标识符(进程id)”来管理他们。
• 现在的linux,同时运行的进程数量一般没有具体限制。
查看进程
1 ps 查看当前进程
2 ps -ef 查看当前进程的所有详细信息
3 ps -af 查看当前所有终端上进程的所有详细信息
4 ps -ef | grep 进程名
5 cat /proc/sys/kernel/pid_max 查看当前最大可同时运行的进程数量
列的含义说明
UID:执行该进程的用户id
PID:进程标识符(进程id)
PPID:该进程的父进程的id,如果一个程序的父级进程找不到,该程序的进程被称为僵尸进程。
C:cpu的占有率,形式是百分数(%)
STIME:进程的启动时间
TTY:终端设备,发起该进程的设备标识符号,如果显示“?”表示该进程不是由终端发起
TIME:进程的执行时间
CMD:该进程的名称或对应的路径
创建进程
system函数
在代码中使用字符串的形式运行shell命令(在这个过程中,会使用shell去启动一个对应的进程,比如pwd,ps,ls等)
1 system("pwd")
system 的局限性
• system由局限性,程序需要等待由system函数启动的进程结束后才能继续,因此不能立刻执行其他任务。
• 使用system函数并非启动其他进程的理想手段,因为它必须用一个shell来启动需要的程序。
• 由于在启动程序之前需要启动一个shell,而且对shell的安装情况及使用的环境的依赖也很大。
因为system需要等待进程结束才能进行,并且还要依赖shell,所以system有很大的局限性,因此我们可以使用exec函数(或者叫exec函数族)。
exec系列函数(exec族函数,簇)
exec函数簇可以根据指定的文件名或目录名找到可执行文件,并用它来原调用进程的数据段,代码段和堆栈段。在执行完后,原调用进程的内容除了进程号外,其他全部内容被新程序的内容替换了。
不同的exec函数参数以及启动方式都会有所不同,具体使用场景可以根据自己的需求来选择。
exec指定的新进程,可以在函数中以参数的形式传递,可以直接指定任意的可执行程序。
exec应用场景
1、当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec函数让自己重生。
2、如果一个进程想执行另外一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。
函数声明、
头文件
#include <unistd.h>
函数原型
int execl(const char *path, const char *arg, ..., NULL);
int execlp(const char *file, const char *arg, ..., NULL);
int execle(const char *path, const char *arg, ..., NULL, char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
函数原型的区别和说明
execl、execlp和execle的参数个数是可变的,参数以一个空指针结束
execv和execvp的第二个参数是一个字符串数组。
e 参数必须带环境变量部分,环境变量部分参数会成为执行exec函数期间的 环境变量, 比较少用
l 命令参数部分必须以"," 相隔, 最后1个命令参数必须是NULL
v 命令参数部分必须是1个以NULL结尾的字符串指针数组的头部指针. 例如 char * pstr就是1个字符串的指针, char * pstr[] 就是数组了, 分别指向各个字符串.
p 执行文件部分可以不带路径, exec函数会在$PATH中找
函数说明
path参数:指向想要执行的程序路径,需要取执行命令的完整路径 比如“/bin/ls”
file参数:可以直接填写文件名,前提是要配置过环境变量,系统就会自动从环境变量“$PATH”所指出的路径中进行查找
arg命令参数:需要传递到程序可执行程序内的参数,要以NULL结尾
返回值
执行成功0,失败返回-1
fork函数
调用fork可以创建一个新进程,fork属于系统调用,会复制当前进程,在进程表中创建新的进程表,新进程除了有不通过的数据空间、环境和文件描述符意外,其他的都和原进程一模一样。新的进程我们称之为子进程。
我们经常把fork和exec函数结合在一起使用
头文件
#include <sys/types.h>
#include <unistd.h>
函数原型
pid_t fork(void);
返回值
如果fork成功,则在父进程中返回子进程的PID,子进程会返回0
如果失败,在父进程中返回-1,没有创建子进程
fork函数与exec的区别
1、fork用于创建一个新的进程,称之为子进程,而exec则是用指定的程序替换当前进程的全部内容。
2、exec的前后进程id没有改变,而fork函数重新创建了子进程后id改变了。
perror("fork 失败") //上一个函数发生错误的原因输出到标准设备
exit(0)//正常运⾏程序并退出程序
exit(1)//⾮正常运⾏导致退出程序
getpid()//查看当前进程pid
wait
wait系统调用可以暂停父进程,直到它的子进程结束为止。
头文件
#include <sys/types.h>
#include <sys/wait.h>
函数原型
pid_t wait(int *stat_loc);
参数说明
stat_loc 我们在使用wait的时候需要声明一个int值,并且把地址传给wait,
wait会将当前结束的这个子进程的一些状态信息写入到stat_loc中,
会返回已经运行结束的子进程的pid。
stat_loc扩展
stat_loc具体说明
如果子进程调用exit退出,那么内核把exit的返回值存放到这个整数变量中;
如果进程是被杀死的,那么内核将信号序号存放在这个变量中。
这个整数由3部分组成,8个bit记录子进程exit值,7个bit记录信号序号,另一个bit用来指明发生错误并产生了内核映像(core dump)。
如果有需要的话,我们可以使用如下方式,从stat_loc中获取这几个值。
high_8 = stat_loc >> 8;
low_7 = stat_loc & 0x7F;
bit_7 = stat_loc & 0x80;
printf("high_8 is %d, low_7 is %d, bit_7 is %d\n", high_8, low_7, bit_7);
使用宏定义解释状态信息
进程间通信
IPC(inter_Process Communication,进程间通信),在不同的进程间进行数据传输(信息交换)的过程,称之为进程间通信。
进程间通信分类
根据传输数据的大小,我们可以将进程通信分为
• 低级通信机制(效率低,例如信号量机制)
• 高级通信机制(OS封装了细节,直接高效使用原语,在进程之间要传递大量数据时,应当使用高级通信机制)
通信机制也可以按照通信类型分为四大类
共享储存器系统
• 基于共享数据结构的通信方式(使用于传递少量数据数据的场景,使用效率低,属于低级通信)
• 基于共享储存区的通信方式
管道通信系统
管道是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件(pipe文件)。
管道机制需要提供一下几点的协调能力:
1、互斥,即当一个进程正在对pipe执行读/写操作时,其它进程必须等待。
2、同步,当一个进程将一定数量的数据写入,然后就去睡眠等待,直到读进程将数据取走,再去唤醒。读进程与之类似。
3、确定对方是否存在。
消息传递系统
• 直接通信方式:发送消息的进程利用系统提供的发送原语直接把消息发送目标进程。
• 间接通信方式:发送和接受进程都通过共享实体(邮箱)的方式进行消息的发送和接收。
客户机进程通信方式
1、套接字(socket):与其他通信机制不同的是,socket可以用于不同机器间的进程通信。
管道通信
一般说的“管道通信”,都是默认说无名管道。
管道通信特点
• 具有固定的读端和写端,所以数据只能在一个方向上流动,我们称之为“半双工”。
• 只能用于具有亲缘关系的进程之间的通信(父子进程、兄弟进程)。‘
• 可以看成是一个特殊的文件,我们也可以对他进行read、write等函数操作。但它区别于普/通文件,不属于任何文件系统,只不过是存在于内存之中。
原理
管道可以分为pipe(无名管道)和fifo(命名管道)两种,除了使用时的建立、打开、删除的方式不同外,两种管道几乎是一样的。
管道的实质是一个内核缓冲区,进程以先进先出,进程以先进先出的方式从缓冲区存取数据:管道一端进程顺序的将进程数据写入缓冲区,另以端的进程则顺序的读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后在缓冲区就不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入时,就唤醒等待队列中的进程继续读写。
无命管道(pipe)
用于相关进程之间的通信,比如父子进程、兄弟进程。通过pipe()系统调用用来完成创建并打开,当最后一个使用它的进程关闭对他的引用时,pipe会自动撤销。
头文件
#include <unistd.h>
函数原型
int pipe(int pipefd[2]);
参数说明
pipefd[2] 用于返回两个指向管道末端的文件描述符。
Pipefd[0]指的是管道的读端。Pipefd[1]指的是管道的写入端。
写入管道的写入端数据由内核进行缓冲,直到从管道的读取端读取为止
返回值
该函数在数组中填上两个新的文件描述符后返回0,如果失败则返回-1并设置errno来表明失败的原因。 perror();
EMFILE:进程使用的文件描述符过多。
ENFILE:系统的文件表已满。
EFAULT:文件描述符无效。
命名管道(named pipe)
无名管道只能在相关进程间通信,但如果想要在任意的进程间交换数据,无名管道不能够适用。我们可以通过命名管道来做这件事。
命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要访问该路径,就能够彼此通过FIFO相互通信,因此,通过FIFO不相关的进程也能交互数据。
有名管道的文件仅仅是作为传输数据的管道,它并不存放传输的数据。
创建管道文件
使用命令:在当前目录下创建了一个名为name的命名管道
mkfifo name
mknod name p
使用函数创建
头文件
#include <sys/types.h>
#include <sys/stat.h>
函数原型
int mkfifo(const char *pathname, mode_t mode);
参数说明
pathname: 路径名,即创建后有名管道的路径名
mode: 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同
返回值:
0:成功。
-1:失败,文件已存在,或出错。
创建管道文件后,适用文件IO系统调用函数通过文件名打开已经创建的命名管道
函数原型
int open( const char * pathname, int flags);
int open( const char * pathname,int flags, mode_t mode);
参数flags 常用的标识:
O_RDONLY 以只读方式打开文件 read only
O_WRONLY 以只写方式打开文件 write only
O_RDWR 以可读写方式打开文件。read write
上述三种标识是互斥的,也就是不可同时使用,但可与下列的标识利用OR(|)运算符组合。
O_CREAT 若欲打开的文件不存在则自动建立该文件。
O_TRUNC 若文件存在并且以可写的方式打开时,此标识会令文件长度清为0,而原来存于该文件的资料也会消失。
O_APPEND 当读写文件时会从文件尾开始移动,也就是所写入的数据会以附加的方式加入到文件后面。
O_NONBLOCK 以不可阻塞的方式打开文件,也就是无论有无数据读取或等待,都会立即返回进程之中。
O_EXCL: 如果同时指定 O_CREAT,而该文件又是存在的,报错;也可以测试一个文件是否存在,不存在则创建。
//open调用将被阻塞,除非有一个进程以写的方式打开同一个fifo
int open( const char * pathname, O_RDONLY);
//即使没有其他进程以写方式打开fifo,这个open调用也会成功并立刻返回(不会阻塞)
int open( const char * pathname, O_RDONLY | O_NONBLOCK);
//open调用将阻塞,直到有一个进程以读的方式打开同一个fifo
int open( const char * pathname, O_WRONLY);
//总是立刻返回,但如果没有进程以读的方式打开fifo,open调用将返回一个错误,并且fifo也不会被打开;
//如果有进程以读的方式打开fifo文件,我们就可以正常对fifo进行写操作
int open( const char * pathname, O_WRONLY | O_NONBLOCK);
O_NONBLOCK分别搭配O_RDOLY和O_WRONLY在效果上不同,如果没有进程以读方式打开管道,非阻塞写方式的open调用将失败,但非阻塞读方式的open调用总是成功。
close调用的行为并不O_NONBLOCK标志的影响。
文件权限
Linux 系统中采用八进制表示权限,如0755,0644
1 ls -l //该命令可以查看文件的操作权限
开头第一位
表示:普通文件(regular)
d 表示:目录文件(directory)
l 表示:链接文件(link)
b 表示:块设备文件(block)
c 表示:字符设备文件(character)
s 表示:套接字文件(socket)
p 表示:管道文件(pipe)
rwx (r:读 w:写 x:执行)
每一个文件权限,都有三组rwx分别对应以下几种用户rwx权限
第一组:该文件拥有者的权限
第二组:该文件拥有者所在组的其他人员对该文件的操作权限
第三组:其他用户组的成员对该文件的操作权限。
举个例子:
• -rwxrwxrwx:一个普通文件,所有用户都对它有读,写,执行文件。
• prw-rw-r--: 一个管道文件,第一组与第二组可读写但不可执行,第三组只读。
上面的表达方式可以换算成二进制,再转成八进制
先换成二进制,有权限就换算成1,没权限就用0表示;再由二进制换算成八进制。
• -rwxrwxrwx:第一位可以用0表示,剩余三个用户组都可读可写可执行,所以换算为二进制0,111,111,111,对应八进制为0777
• prw-rw-r--: 第一位可以用0表示,第一组与第二组可读写但不可执行,第三组只读,换算为二进制:0,110,110,100,对应八进制为0664
消息队列(messagequeue)
消息队列,可以理解为是一个存放消息(数据)容器。将消息写入消息队列,然后再从消息队列中取消息,一般来说是先进先出的顺序。
• 消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
• 如果没有释放消息队列或者关闭操作系统,消息队列会一直存在,而匿名管道是随进程的创建而建立,随进程的结束而销毁。
• 每个数据块存储节点都有一个最大长度的限制,系统中所有列所包含的全部数据块的总长度也有一个上限
消息队列对比管道的优势
• 消息队列适合频繁地交换数据
• 消息队列可以指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
使用流程
1、ftok函数生成键值
2、msgget函数创建消息队列
3、msgsnd函数往消息队列发送消息
4、msgrcv函数从消息队列读取消息
5、msgctl函数进行删除消息队列
msgget
创建或者打开一个消息队列
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型
int msgget(key_t key, int msgflg);
参数说明
key:0(IPC_PRIVATE):会建立新的消息队列
大于0的32位整数:视参数msgflg来确定操作。通常要求此值来源于ftok返回的IPC键值
msgflg:
IPC_CREAT:当msgflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列,返回此消息队列的标识符
IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列则报错
返回值
成功:返回消息队列的标识符
出错:-1,错误原因存于error中
EACCES:指定的消息队列已存在,但调用进程没有权限访问它
EEXIST:key指定的消息队列已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志
ENOENT:key指定的消息队列不存在同时msgflg中没有指定IPC_CREAT标志
ENOMEM:需要建立消息队列,但内存不足
ENOSPC:需要建立消息队列,但已达到系统的限制
msgctl
获取和设置消息队列的属性
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
参数说明
msqid:消息队列标识符
cmd:IPC_STAT:获得msgid的消息队列头数据到buf中
IPC_SET:设置消息队列的属性,要设置的属性需先存储在buf中,可设置的属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes
IPC_RMID:删除消息队列 remove id
buf:消息队列管理结构体,请参见消息队列内核结构说明部分
返回值
成功:返回消息队列的标识符
出错:-1,错误原因存于error中
EACCESS:参数cmd为IPC_STAT,无权限读取该消息队列
EFAULT:参数buf指向无效的内存地址
EIDRM:标识符为msqid的消息队列已被删除
EINVAL:无效的参数cmd或msqid
EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行
msgsnd
将消息写入到消息队列
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:将msgp消息写入到标识符为msqid的消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
参数说明
msqid:消息队列标识符
msgp:发送给队列的消息。msgp可以是任何类型的结构体,但第一个字段必须为long类型,即表明此发送消息的类型,msgrcv根据此接收消息。
msgp定义的参照格式如下:
struct s_msg{ /*msgp定义的参照格式*/
long type; /* 必须大于0,消息类型 */
char mtext[256]; /*消息正文,可以是其他任何类型*/
} msgp;
msgsz:要发送消息的大小
msgflg:0:当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列
IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回
IPC_NOERROR:若发送的消息大于size字节,则把该消息截断,截断部分将被丢弃,且不通知发送进程。
返回值
成功:返回消息队列的标识符
出错:-1,错误原因存于error中
EAGAIN:参数msgflg设为IPC_NOWAIT,而消息队列已满
EIDRM:标识符为msqid的消息队列已被删除
EACCESS:无权限写入消息队列
EFAULT:参数msgp指向无效的内存地址
EINTR:队列已满而处于等待情况下被信号中断
EINVAL:无效的参数msqid、msgsz或参数消息类型type小于0
msgsnd()为阻塞函数,当消息队列容量满或消息个数满会阻塞。
msgsnd()解除阻塞的条件有以下三个条件:
1、消息队列中有容纳该消息的空间。
2、msqid代表的消息队列被删除。
3、调用msgsnd函数的进程被信号中断。
msgrcv
从消息队列读取消息
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:从标识符为msqid的消息队列读取消息并存于msgp中,读取后把此消息从消息队列中删除
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数说明
msqid:消息队列标识符
msgp:存放消息的结构体,结构体类型要与msgsnd函数发送的类型相同
msgsz:要接收消息的大小
msgtyp: 0:接收第一个消息
>0:接收类型等于msgtyp的第一个消息
<0:接收类型等于或者小于msgtyp绝对值的第一个消息
msgflg:0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息
IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃
返回值
成功:实际读取到的消息数据长度
出错:-1,错误原因存于error中
E2BIG:消息数据长度大于msgsz而msgflag没有设置IPC_NOERROR
EIDRM:标识符为msqid的消息队列已被删除
EACCESS:无权限读取该消息队列
EFAULT:参数msgp指向无效的内存地址
ENOMSG:参数msgflg设为IPC_NOWAIT,而消息队列中无消息可读
EINTR:等待读取队列内的消息情况下被信号中断
msgrcv()解除阻塞的条件有以下三个:
1、消息队列中有了满足条件的消息。
2、msqid代表的消息队列被删除。
3、调用msgrcv()的进程被信号中断。
信号(sinal)
信号是UNIX和Linux系统响应某些条件而产生的一个事件。接收到信号的进程会相应的采取一些行动。
可作为进程间传递消息的一种方式,信号可以被生成、捕获、响应或忽略。
上面说的进程间的通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用【信号】的方式来通知进程。
信号和信号量虽然名字相似度66.6%,但两者的用途完全不一样。
在Linux操作系统中,为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过kill -l命令,查看所有的信号:
kill
使用kill可以发送信号
int kill(pid_t pid, int sig)
kill函数把sig给定的信号发送给参数pid所指定的进程。
signal
signal注册信号接收
头文件
#include <signal.h>
函数原型
sighandler_t signal(int signum,sighandler_t handler);
参数说明
signum:要捕捉的信号
handler:捕捉到如何处理
SIG_IGN:忽略
SIG_DFL:使用信号默认行文
回调函数:这个函数是内核调用
共享内存(shared memory)
共享内存的机制,就是拿出一块虚拟空间地址来,映射到相同的物理内存中,这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
采用共享内存进行通信的一个主要好处就是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝,对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次数据拷贝,而共享内存只需要拷贝两次:一从输入文件到共享内存区,另一次从共享内存到输出文件。
共享内存没有同步机制(第一个进程操作完毕前,无法阻止第二个进程访问共享内存)
shmget
创建共享内存
头文件
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型
int shmget(key_t key, size_t size, int shmflg);
参数说明
key:提供一个参数key(非0整数),它有效地为共享内存段命名
size:以字节为单位指定需要共享的内存容量
shmflg:权限标志
返回值:成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.
shmat
第一次创建共享内存时,它还不能被任何进程访问,shmat函数就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
头文件
#include <sys/types.h>
#include <sys/shm.h>
函数原型
void *shmat(int shm_id, const void *shm_addr, int shmflg);
参数说明
shm_id:由shmget函数返回的共享内存标识。
shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
shm_flg:一组标志位,通常为0。
返回值:这段共享内存空间的实际地址,如果出错-1
shmdt
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前内存不再可用。
头文件
#include <sys/types.h>
#include <sys/shm.h>
函数原型
int shmdt(const void *shmaddr);
参数说明
shmaddr:shmat函数返回的地址指针
返回值:调用成功时返回0,失败时返回-1
shmctl
用来控制共享内存
头文件
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型
int shmctl(int shm_id, int command, struct shmid_ds *buf);
参数说明
shm_id:shmget函数返回的共享内存标识符
command:要采取的操作,可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段
buf:一个结构指针,它指向共享内存模式和访问权限的结构、
信号量(semophore)
信号量(semophore)是操作系统用来解决并发中的互拆和同步问题的一种方法,信号量可以看成是一个计数器。
临界区(共享资源)
● 临界区指的是一个访问公共资源的程序片段,这些公共资源又无法同时被多个进程同时访问。当有进程进入临界区域访问时,其他线程或者进程必须等待,以确保这些公共资源被互拆获得使用。
● 每个进程中访问临界资源的那段代码称为临界区(Critical Section),每次只允许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,多个进程必须互拆的对它进行访问。
信号量工作原理
信号量是一个特殊的变量,程序对其访问都是原子操作(要么发生、要么不发生),且只允许对它进行“等待”和“发送”消息操作。
最简单的信号量只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以去多个正整数的信号量被称为通用信号量。
信号量数目
n>0:当前有可用资源,可用资源数量为n。
n=0:资源都被占用,可用资源数量为0。
n<0:资源都被占用,并且还有n个进程正在排队。
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),它们的行为是这样的:
● P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。
● V(sv):如果有其他进程因进程等待sv被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1。
原子操作
一个程序在执行的时候可能被优先级更高的线程中断。而有些操作是不能被中断的,不然会出现无法还原的后果,这时候,这些操作就需要原子操作。就是不能被中断的操作。
semget
获取一个已存在的、或创建一个新的信号量
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semget(key_t key, int num_sems, int sem_flags);
参数说明
key 对应一个唯一的信号量,不同的进程可通过该键值和semget获取唯一的信号量(IPC_PRIVAT该信号量只允许创建者本身,可用于父子进程间通信)
num_sems 需要的信号量数目
sem_flags
IPC_CREAT, 如果该信号量未存在,则创建该信号量如果该信号量已存在,也不发送错误。
IPC_EXCL只有信号量不存在的时候,新的信号量才建立,否则就产生错误。
返回值: 成功, 则返回一个信号量标识符(正数)
失败, 返回-1
semop
改变信号量的值,即对信号量执行P操作、或V操作 。
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semop(int semid, struct sembuf *sops, size_t nsops);
参数说明
semid 信号量标识符, 也就是semget的返回值
sops
struct sembuf{
short sem_num; //除非使用一组信号量,否则它为0
short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数,
//一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
nsops 第二个参数sops所表示的数组的大小、表示有几个struct sembuf
semctl
删除和初始化信号量
头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型
int semctl(int sem_id, int sem_num, int command, ...);
参数说明
sem_id:信号量标识符
sem_num:信号量组中的编号,如果只有一个信号量,则取0
command:SETVAL:用来把信号量初始化为一个已知的值。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
如有需要第四个参数一般设置为union semnu
arg;定义如下
union semun{
int val; //使用的值
struct semid_ds *buf; //IPC_STAT、IPC_SET 使用的缓存区
unsigned short *arry; //GETALL,、SETALL 使用的数组
struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};
返回值: 出错返回-1
根据不同的场景,返回不同的值。