全部博文(51)
分类: LINUX
2015-03-27 16:06:58
大型的应用系统,有两种方式可以建立:
1、通过一个孤立的、大型的、复杂的进程实现全部功能;如见日益复杂,规模庞大,该方法已不现实
2、通过若干相互联系的、小型的、相对简单的进程构成的组合来提供所需功能。
第二种方法好处:
1、软件模块化,每个进程分别设计、实现、调试、维护;
2、每个进程独立的地址空间,相互通信通过进程间通信手段完成,相当程度上排除了相互干扰的可能性,增加系统的可靠性和稳定性;
3、系统具有可扩充性;
4、复用性
第二种方法缺点:
1、占用更多资源,增加CPU运行时开销;(硬件发展,该影响已无大影响)
2、进程独立调度,运行时序某些情况下会成为问题,需要特殊进程间通信手段保持同步;
3、OS提供充分的进程间通信手段和措施
进程间通信在现代操作系统中起了至关重要的作用。
早起unix方法:
1、管道(pipe),父子进程或者两个兄弟进程之间通信,由父进程建立,单向的;
2、信号(signal),严格来说不是为专用的进程间通信设计的,也用于内核与进程间通信。是对“中断”概念在软件层次上的模拟;
3、跟踪(trace),通过系统调用ptrace()读/写子进程地址空间中的内容,从而达到跟踪子进程的目的。
以上几乎只能用于父子/兄弟进程,信号虽然未限制,但需要知道对方pid,一般只有父子进程之间才知道对方pid。
非近亲进程之间
1、命名管道(named pipe),以FIFO文件的形式出现在文件系统中,任何进程都可以使用文件名来打开改管道。
AT&T的UNIX系统V中,新增三种进程间通信手段,成为"system V IPC"
1、报文(message),通过系统调用设立一个报文队列,任何进程都可以通过系统调用向这个队列发送消息或者从队列接收消息,进程间以报文传递形式实现进程间通信;
2、共享内存,一个进程通过系统调用设立一片共享内存区,其他进程就可以通过系统调用将该存储区映射到用户地址空间中,可以想正常的内存访问一样读写该共享区间了。是一种快速有效的进程间通信手段。不像其他手段那样,可以在一旦写入后就唤醒正在睡眠中等待读取的进程,所以常常需要与其他手段配合使用。
3、信号量(semaphore)
BSD UNIX也对此做了重要的扩充:
1、插口(socket),与命名管道相似,socket不仅可以用来实现同一台计算机上的进程间通信,还可以用来实现分布式于不同计算机中的进程通过网络进行通信。
管道具有以下特点:
(1)管道是半双工的,数据只能单向流动;需要相互通信时,就要建立两个管道。
(2)只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程,有名管道则突破了这一限制)。
(3)单独构成一种独立的文件系统,并且只存在于内存中。
(4)数据的读出和写入都是单向的:一个进程向管道中写的数据被管道另一端的进程读出。写入的数据每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
管道的创建:
int pipe(int fd[2])
描述字fd[0]表示,即管道读端;由描述字fd[1]表示,即管道写端。
1. 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
a父进程创建管道
b 父子进程共享管道
c父子进程通过管道单向通信
d兄弟进程通过管道单向通信
系统调用入口sys_pipe()
创建管道的过程主要是创建文件的过程。这是一个特殊文件,实质是一个用作缓冲区的内存页面,纳入了文件系统的机制,借助文件系统的各种数据结构和操作加以管理。
创建两个file数据结构,这种数据结构代表一个特定进程对某个文件操作的现状。这两个file结构的文件操作函数的指针分别对应读/写函数。
管道是特殊文件,所以还必须有inode数据结构,记录文件几乎所有的属性信息,所属用户,设备、文件的创建,修改,访问时间等信息。
file结构与inode结构之间的桥梁目录项dentry数据结构
管道是特殊的文件系统
两个进程之间是典型的生产者/消费者关系:
对于生产者,缓冲区有空间则往里写,并且唤醒可能正在等待着要从缓存区中读取数据的消费者;没有空间继续睡眠,等待消费者腾出空间;
对于消费者,缓冲区有数据就读,然后唤醒可能等待的生产者,若无数据则睡眠,等待生产者写数据。
一般是生产者先调用exit(),消费者调用exit()在后;
特殊条件下,消费者exit()在前,内核会向生产者进程发出一个SIGPIPE信号,表示管道断裂,生产者接到信号后通常就exit()。
管道不可能在任意两个进程之间建立通信,其缺点暗示人们只要有名,有形的管道就可以克服这种缺点。
有名:拥有文件名;
有形:文件inode应该存在磁盘或者其他文件系统介质上。
命名管道在普通文件,块设备文件、字符设备文件之外,又设立了FIFO文件。严格遵循先进先出原则,不允许有在文件内移动读写指针的位置lseek()操作。
与管道最大区别:管道无需打开文件的操作;使用命令管道进程必须有open操作。
其他读、写、及关闭操作与普通管道完全相同。
注意FIFO文件的inode节点在磁盘上,但是那只是一个节点,而文件数据只存在于内存缓存页面中。
信号(软中断)机制是软件层次上对中断机制的一种模拟。在所有的进程间通信机制中只有信号是异步的。与中断机制类似,早期没有充分吸收在中断处理方面的经验,开始的信号机制较简单原始,称为不可靠信号。
信号向量表
struct signal_struct {
atomic_t count;
struct k_sigaction action[_NSIC];
spinlock_t siglock;
};
所指向的处理程序一般都在用户空间。
对信号的检测和响应总是发生在系统空间,有两种情况:
1、当前进程由于系统调用、中断或者异常进入到系统空间后,由系统空间返回到用户空间前夕;
2、当前进程在内核进入睡眠后刚被唤醒的时候,由于信号的存在而提前返回到用户空间。
struct sigaction{
union {
--sighandler_t _sa_handler;
void (* _sa_sigaction)(int, struct siginfo *, void *)
}_u;
sigset_t sa_mask;
unsigned long sa_flags;
void (*sa_restorer)(void);
};
sa_restorer现在基本不用了;
sa_mask是一个位图,每一位对应一种信号。表示在执行当前信号的处理程序期间要将相应的信号暂时“屏蔽”,一旦屏蔽去除,已经到达的信号仍旧还在,屏蔽使得在执行过程中不会嵌套相应这种信号。借鉴了中断服务中关闭中中断以防止嵌套的经验。这将不可靠信号改进成了可靠信号的关键一步。
#define _NSIG 64
#define _NSIG_BPW 32
#define __NSIG_WORDS (_NSIG / _NSIG_BPW)
typedef unsigned long old_sigset_t;
typedef struct {
unsigned long sig[_NSIC_WORDS];
}sigset_t;
typedef struct siginfo {
......
}siginfo_t; //信号产生的相关信息
struct sigpending {
struct sigqueue *head, **tail;
sigset_t signal;
};
没产生一个信号就将其挂入这个队列,保证信号不丢失。
在对信号做出改进之前,已经开发了较多使用信号的软件,为保持兼容性,在保留原先已经定义的信号不变的情况下,再定义一些新信号。
设置信号向量,也就是信号处理程序的安装,有三个系统调用,sys_signal(), sys_sigaction(),sys_rf_sigaction():
1、sighandler_t signal (int signnm, sighandler_t handler);
其他两个系统调用是新的,但在用户程序设计界面上却为相同的库函数出现:
int sigaction(int signum, const struct sigaction * newact, struct *sigaction oldact);
从UNIX早期版本开始就提供了一种对运行中的进程进行跟踪和控制的手段,即系统调用ptrace(),一个进程可以动态的读/写另一个进程的内存和寄存器,包括:指令空间、数据空间、堆栈以及所有的寄存器。与信号机制结合可以实现让一个进程在另一个进程的控制和跟踪下运行。
ptrace所实现的“通信”完全是单方面的,从这个角度来讲,它不属于进程通信。
init进程是不允许跟踪的;
实施跟踪的条件:
1、 自己不允许跟踪自己;
2、 两个进程属于同一用户或同一组;
3、 两进程不在同一组,当前进程提升为特权用户。
管道的缺点:
1、 所传诵的是无格式的字节流;
2、 无格式的字节流,导致缺乏一些控制手段;
3、 管道机制的缓冲区大小是有限制的,静态的;
4、 从运行效率看,管道机制的运行开销不小;
5、 每个管道都占用一个打开的文件号。
AT&T在其UNIX系统V版本中增加了报文传递、共享内存以及信号量三种IPC机制,统称为“系统V进程间通信机制”。
每一个内核中的IPC结构都用一个非负整数的标识符(identifier)加以引用。标识符是IPC对象的内部名;键(key)则是外部名。
IPC结构在系统范围内起作用,没有访问计数,若无进程进行删除/读取信息操作,则在系统再次启动前一直余留在系统中。
LINUX内核为系统V IPC提供一个统一的系统调用ipc():
int ipc (unsigned int call, int first, int second, int third, void *ptr, int firth)
第一个参数call为操作码,
#define SEMOP 1
#define SEMGET 2
#define SEMCTL 3
#define SEMTIMEDOP 4
#define MSGSND 11
#define MSGRCV 12
#define MSGGET 13
#define MSGCTL 14
#define SHMAT 21
#define SHMDT 22
#define SHMGET 23
#define SHMCTL 24
SEM开头为信号量设置;MSG为报文传递设置;SHM为共享内存设置。
C库提供了semget(), msgget(),msgsend()进行封装。
int msgget(key_t key, int flag)
其功能是打开一个现场队列或创建一个新队列。
ket_t实际是一个整数,每个报文队列键值必须唯一。一特殊情况,每个进程都可以使用键值0(IPC_PRIVATE)建立一个供自己私用的报文队列。
关于报文队列的个数:初始值为16,大小可以扩充,最多为32768,数组大小只扩展不会缩小。
一体化的标识号:最初找到空闲的标识号是数组下标,这中标号在特定时刻是唯一的,但是观察一个合理长的时间跨度就不一定是唯一的。所以又根据一个序号seq,和下标编码形成一个一体化的标识号。
int msgsend(int msgqid, const void *ptr, size_t nbytes, int flag)
ptr参数指向一个长整形数,它包含了正的整形消息类型,在其后紧跟消息数据。
Struct msmesg
{
Long mtype;
Char mtext[X];
};
Flag 可以指定为IPC_NOWAIT。队列已满、消息总数等于系统限制、消息队列中字节总数等于系统限制,返回错误EAGAIN;
若没有指定IPC_NOWAIT,进程阻塞直到下述情况出现:空间可以容纳要发送的消息;系统中删除此队列(EIDRM),捕捉到一个信号(EINTR)。
图一报文队列结构连接示意图
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag)
返回值:若成功则返回消息的数据部分的长度,若出错则返回-1
参数type:
type == 0 返回队列中的第一个消息;
type > 0 返回队列中消息类型为type的第一个消息;
type < 0 返回队列中消息类型小于或者等于type绝对值的消息,如果消息类型存在若干个,则取类型最小的消息;
flag指定为IPC_NOWAIT,操作不阻塞,没有指定类型的消息,返回ENOMSG;
没有指定IPC_NOWAIT,进程阻塞直至:有了指定类型的消息;系统中删除此队列(EIDRM),捕捉到一个信号(EINTR)。
int msgctl(int msqid, int cmd, structmsqid_ds *buf)
参数cmd命令码:
#define IPC_RMID 0 /* remove resource */
#define IPC_SET 1 /* set ipc_perm options */
#define IPC_STAT 2 /* get ipc_perm options */
#define IPC_INFO 3 /* see ipcs */
共享内存机制中,不同进程读/写一块内存空间的操作本身就是微观、直接的,失去了由内核保证互斥性的可能。进程间不会自动的同步,并且所写的内容在全部完成前就立刻可以部分地为其他进程所看到。
int shmget(key_t key, size_t size,int flag)
size共享存储段的长度,通常取系统页长的整数倍,若创建一个新段,则必须指定其size;如果引用一个现存的段,则size为0。
共享内存区中的页面和普通的页面一样,受到内存页面管理机制的调度,根据实际需要换入换出,共享内存区各自设立专用的映射文件,以共享内存区的区名作为文件名。共享内存区则成为文件映射的一项应用。
内核中专门设立了一种特殊的文件系统“shm”。该文件系统是有形的,需要在文件中实际的存储数据,需要落实到物理外设上;共享内存区文件只对系统的当前运行有意义,关机不应继续存在,不合适放在普通文件系统中,所以存放在页面交换盘区中。
建立共享内存区的问题转变为在特殊文件系统shm中建立映射文件的问题。
void *shmat (int shmid, const void *addr, int flag)
addr为0,则此段连接到内核选择的第一个可用地址上;
addr非0,并且没有指定SHM_RND,则此段连接到addr所指定的地址上;
addr非0,并且指定了SHM_RND,此段连接到(addr-(addr mod ulus SHMLBA))所表示的地址上。SHMLBA“低边界地址倍数”。
shmat创建的内存区映射到本进程的虚拟空间,此外,一个已经映射的共享内存区域也可以通过该函数改变其映射空间。
do_mmap()建立起文件与虚拟空间的映射。
int shmdt(void *addr)
当对共享存储段的操作已经结束时,调用shmdt脱接该段。
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
信号量是一个计数器,用于多进程对共享数据对象的访问。
为获取共享资源,进程需要执行以下操作:
1、测试控制该资源的信号量;
2、此信号量为正值,该资源可以使用,信号量减一;
3、信号量值为0,进程进入休眠状态,直至信号量值大于0,进程被唤醒。返回至第一步。
信号量特点:
1、信号量集为含有一个或多个信号量的集合,当创建时指定该集合中信号量的数量;
2、创建信号量集(semget)和对其赋初值(semctl)分开;缺点;不能原子的创建信号量集;
3、没有进程使用任然存在。有些进程终止时没有释放分配的信号量,undo功能。
int semget(key_t key, int nsems, int flag)
nsems集合中的信号量数,若创建新集合,必须指定nsems;如果引用一个现有的集合,则将nsems指定为0。
int semctl(int semid, int semnum, int cmd,
… /*union semun arg*/)
Union semun
{
int val; /*for SETVAL*/
struct semid_ds *buf; /*for IPC_STAT and IPC_SET*/
unsigned short *array; /*for GETALL and SETALL*/
}
int semop(int semid, struct sembuf semoparray[], size_t nops)
struct sembuf
{
unsigned short sem_num; /*member # in set(0,1,2,…,nsems-1)*/
short sem_op; /*operation (negative, 0 , or positive)*/
short sem_flag; /*IPC_NOWAIT, SEM_UNDO*/
}
1、sem_op为正,进程释放占用的资源数。Sem_op值加到信号量值上。如果指定了undo标志,从该进程的信号量调整值中减去sem_op;
2、sem_op为负,获取该信号量控制的资源,从信号量值中减去sem_op的绝对值,如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。
3、sem_op为0,表示调用进程希望等待到该信号量值变为0。
exit时的信号调整,若进程终止时,占用了经由信号量分配的资源,就无法再释放。若设置了SEM_UNDO标记,无论自愿与否,内核都会按照调整值对相应量值进行处理。
一次SEMOP操作可以是对一个信号量集合的操作,并且必须符合“要么全有,要么全无”的原则
1、linux管道通信(C语言)
2、《Linux内核源代码情景分析》
3、《UNIX环境高级编程》