四,进程间通信
(一),进程间通讯IPC概述
1,进程间通信的原因:
1),数据传输,一个进程需要将它的数据发送给另一个进程;
2),资源共享,多个进程之间共享同样的资源;
3),通知事件,一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件;
4),进程控制,有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。
2,Linux使用的进程间通信方式包括:
1),管道pipe和有名管道FIFO;
2),信号
3),共享内存
4),消息队列
5),信号量
6),套接字
(二),管道通信
1,管道是单向的,先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。
2,数据被一个进程读出后,将被从管道中删除,其它读进程将不能再读到这些数据。管道提供了简单的流控机制,进程试图读空管道时,进程将阻塞。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞。
3,管道分为无名管道和有名管道两种,前者用于父子进程之间的通信,后都用于运行于同一系统中的任意两个进程间的通信。
4,pipe函数,int pipe(int filedis[2])
创建无名管道,当一个管道建立时,它会创建两个文件描述符,filedis[0]用于读管道(指向管道头部),filedis[1]用于写管道(指向管道尾部)。两个进程直接操作这个两个文件描述符就像操作文件一样。关闭管道只需要将这两个文件描述符关闭即可,可以使用普通的close函数逐个关闭。
5,管道用于不同进程间通信,通常先创建一个管道,再通过fork创建一个子进程,该子进程会继承父进程所创建的管道。必须在系统调用fork之前调用pipe创建管道,否则子进程将不会继承文件描述符。
6,命名管道或称有名管道,也叫FIFO。它的无名管道的不同点,无名管道只能由父子进程使用,但是通过有名管道,不相关的进程也能交换数据。
7,mkfifo函数,int mkfifo(const char *pathname, mode_t mode) 创建FIFO。一旦创建了FIFO就可用open打开它,一般的文件访问函数如read,wirte等都可用于FIFO。参数1为FIFO名,参数2为属性。
8,当打开FIFO时,非阻塞标志(O_NONBLOCK)将对以后的读写产生如下影响:
1)没有使用此标志,访问要求无法满足时进程将阻塞,如试图读取空的FIFO,将导致进程阻塞。
2)使用此标志,访问要求无法满足时不阻塞,立刻出错返回,errno是ENXIO。
(三),信号通信
1,信号机制是UNIX系统中最为古老的进程间通信机制,比如按某些键,比如硬件异常,除数为0等。
2,进程使用kill函数将信号发送给另一个进程,用户使用kill命令将信号发送给其它进程。
3,常见的信号如下:
1)SIGHUP:从终端上发出的结束信号;
2)SIGINT:来自键盘的中断信号(Ctrl-C);
3)SIGKILL:该信号结束接收信号的进程;
4)SIGTERM:kill命令发出的信号;
5)SIGCHLO:标识子进程停止或结束的信号;
6)SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止执行信号。
4,当某信号出现时,可以按照以下三种方式中的一种进行处理:
1)忽略此信号,大多数信号都这样,但SIGKILL和SIGSTOP不能被忽略,它们不能被忽略的原因是它们向超级用户提供了一种终止或停止进程的方法。
2)执行用户希望的动作,通知内核在某种信号发生时,调用一个用户函数。在用户函数中,执行用户希望的处理。
3)执行系统默认动作,对大多数信号的系统默认动作是终止该进程。
5,kill函数,int kill(pid_t pid, int signo)
可以向自身发送信号,也可以向其它进程发送信号。参数1是线程的PID,参数2是信号编号。如果PID大于0则将信号发送给指定PID的进程,如果PID等于0则发给同组的进程,如果PID小于0则发给其进程组ID等于PID绝对值的进程,如果PID等于-1则将信号发送给所有进程。
6,raise函数,int raise(int signo)
向进程自身发送信号。
7,alarm函数,unsigned int alarm(unsigned int seconds)
可以设置一个时间值(闹钟),当所设置的时间到了时,产生SIGALRM信号。如果不捕捉此信号,则默认动作是终止该进程。参数是指点seconds秒后会产生信号SIGALRM。
8,每个进程只能有一个闹钟时间。如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,以前登机的闹钟时间则被新值代换。如果有以前登记的尚未超过的闹钟时间,则这次seconds值是0,则表示取消以前的闹钟。
9,pause函数,int pause(void)
使调用进程挂起直至捕捉到一个信号,只有执行了一个信号处理函数后,挂起才结束。
10,当系统捕捉到某个信号时,可以忽略,默认处理,或使用指定的处理函数来处理。信号处理的主要方法有两种,一种是简单的signal函数,另一种是使用信号集函数组。
11,signal函数,void (*signal(int signo, void (*func)(int)))(int)
由于有typedef void (*sighandler_t)(int),所以函数变成
sighandler_t signal(int signum, sighandler_t handler)
func可能的值是:
1)SIG_IGN,忽略此信号;
2)SIG_DFL,按系统默认方式处理;
3)信号处理函数名,使用该函数处理。
(四),共享内存
1,共享内存是被多个进程共享的一部分物理内存。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。
2,共享内存的实现分为两个步骤:
1)创建共享内存,使用shmget函数;
2)映射共享内存,将这段创建的共享内存映射到具体的进程空间去,使用shmat函数。
3,shmget函数,int shmget(key_t key, int size, int shmflg)
创建共享内存。key标识共享内存的键值:0/IPC_PRIVATE。成功则返回共享内存标识符,失败返回-1。
4,shmat函数,int shmat(int shmid, char *shmaddr, int flag)
映射共享内存。shmid为内在标识符,flag决定以什么方式来确定映射的地址,通常为0。如果成功,则返回共享内存映射到进程中的地址,如果失败则返回-1。
5,shmdt函数,int shmdt(char *shmaddr)
当一个进程不再需要共享内存时,把它从进程地址空间中脱离。
(五),消息队列
1,由于信号能够传送的信息量有限,管道则只能传送无格式的字节流,这无疑会给应用程序开发带来不便。消息队列则克服了这些缺点。
2,消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式。进程可以向按照一定的规则添加新消息,另一些进程则可以从消息队列中读走消息。
3,消息队列是随内核持续的,只有内核重起或者人工删除,该消息队列才会被删除,并不会相关应用程序消失而消失。由于其内核持续性所以要求每个消息队列都在系统范围内对应唯一的键值,所以要获得一个消息队列的描述字,必须提供该消息队列的键值。
4,ftok函数,key_t ftok(char *pathname, char proj)
返回文件名对应的键值。参数1为文件名,参数2为项目名(不为0即可)。
5,msgget函数,int msgget(key_t key, int msgflg)
打开/创建消息队列。返回值为与键值key对应的消息队列描述字。参数1为键值,参数2为标志位,如下:
1)IPC_CREAT,创建一个新的消息队列;
2)IPC_EXCL,与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误;
3)IPC_NOWAIT,读写消息队列要求无法得到满足时,不阻塞。
6,以下两种情况下,将创建一个新的消息队列:
1)如果没有与键值key相对应的消息队列,并且msgflg中包含了IPC_CREAT标志位;
2)key参数为IPC_PRIVATE。
7,msgsnd函数,int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg)
向消息队列中发送一条消息。相关参数如下:
1)msqid,已打开的消息队列的ID;
2)msgp,存放消息的结构;
3)msgsz,消息数据长度;
4)msgflg,发送标志,有意思的标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd函数是否等待。
8,消息队列的数据结构struct msgbuf,成员1为mtype消息类型,成员2为mtext[1]为消息数据首地址。
9,msgrcv函数,int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg)
从msqid消息队列中读取一个msgtyp类型的消息,并把消息存储在msgp指向的msgbuf结构中。在成功的读取了一条消息以后,队列中的这条消息将被删除。
(六),信号量
1,信号量与其它进程间通信方式不大相同,主要用途是保护临界资源。进程可以根据它判定是否能够访问某些共享资源。除了访问控制外,还可用于进程同步。
2,信号量分为二值信号量和计数信号量。计数信号量的值可以取任意非负值。二值信号量的值只能取0或1,类似于互斥锁。但信号量强调共享资源,只要共享资源可用,其它进程同样可以修改信号量值,而互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
3,semget函数,int semget(key_t key, int nsems, int semflg)
打开或创建信号量。参数1为键值,由ftok获得,参数2为指定打开或新创建的信号灯集中将包含信号灯的数目,参数3是标志,同消息队列。
4,semop函数,int semop(int semid, struct sembuf *sops, unsigned nsops)
对信号量进行控制。参数1为信号量集的ID,参数2为操作数组,表明要进行什么操作,参数3为参数2所指向的数组的元素个数。其中参数2的结构为struct sembuf,它有三个成员,成员1为sem_num为信号量在集中的编号,第一个信号量的编号为0,成员2为sem_op为信号量操作,如果其值为正数,该值为加到现有的信号量值中,通常用于释放信号量,如果sem_op值为负数,而其绝对值又大于信号量的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值,通常用于获取信号量,如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。成员3为sem_flg为信号操作标志,如果为IPC_NOWAIT则对信号的操作不能满足时,semop函数不会阻塞,并立即返回,同时设定错误信息,如果为IPC_UNDO则程序结束时释放信号量,这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。