1.07k likes | 1.2k Views
第六讲 UNIX 进程通信. 注意:后期课程安排. 第 13 周: 星期二 7-8 节 上课(进程通信) 星期三 5-6 节 上机(进程通信) 星期五 1-2 节 上课( SOCKET) 第 14 周 星期二 7-8 节 上课( SOCKET) 第 15 周 星期二 5-6 节 上机 (实验检查) 星期五 1-2 节 考试. 1 基本概念. 通信分为两类: 控制信息的传递: 低级通信 大批量数据的传递: 高级通信.
E N D
注意:后期课程安排 第13周: 星期二7-8节 上课(进程通信) 星期三5-6节 上机(进程通信) 星期五1-2节 上课(SOCKET) 第14周 星期二7-8节 上课(SOCKET) 第15周 星期二5-6节 上机 (实验检查) 星期五1-2节 考试
1 基本概念 • 通信分为两类: 控制信息的传递: 低级通信 大批量数据的传递: 高级通信
1 基本概念 2.基本的通信方式 (a)主从式通信: 通信的双方存在一种隶属关系, 其中主进程是通信过程的控制者,而从进程是通信过程的从属者。主从式通信具有如下特点: • 在通信过程中主进程对从进程的资源和数据享有使用权限,而从进程对主进程则没有这种权限。 • 在通信过程中汉族要进程始终控制着从进程的工作和动作过程 • 一旦进程的主从关系确定,在正个通信过程中他们的隶属关系不再再发生变化。 例如:终端控制进程和终端进程
1 基本概念 2.基本的通信方式 (b)会话式通信: 通信进程双方采用请求应答的方式进行通信。在会话式通信中,通信的进程双方分为使用者进程个服务者进程。,而使用者进程通过调用服务者进程来完成进程间的通信。 会话式通信的特点: • 通信时使用者进程需要事先得到服务者进程的允许,方能使用服务者进程为其提供的服务。 • 服务者进程每次都是根据使用者进程提出的请求服务的 ,并且在进程通信的过程中控制权始终为服务者进程所有 • 同样进程间在确定使用会话方式 进行通信时也要建立固定的逻辑关联关系。 例如:用户进程与磁盘管理进程 采用TCP/IP协议的网间进程通信
1 基本概念 2.基本的通信方式 (c)消息或邮件通信: 通信双方处于一个平等地位,在通信过程中无论接收进程是否准备好,发送进程都可以进行消息发送。发的消息通过消息系统或邮件系统进行大批量数据传递。消息或邮件通信的特点: • 通信中发送进程能否发送消息,只与消息缓冲区或邮箱中是否有足够大的空闲空间来满足这次通信有关,与需要将信息发送到的目的进程的状态无关 • 发送进程和接收进程之间不需要建立直接的逻辑关联关系 • 发送信息和接收信息必须通过消息缓冲区或邮箱来完成消息的传递。
1 基本概念 进程1 共享存储区 进程2 2.基本的通信方式 (d)共享存储区通信:进程之间采用信息共享存储区的通信方式来完成进程间的通信。信息共享区域是通信进程都可以访问的数据区。 共享存储区的进程通信过程的特点: • 进程通信中,通信的数据或信息不发生存储移动 • 当需要交互时,通信进程双方通过一个共享存储区完成信息交互 • 共享存储中的数据,可以作为需要交互进程的一部分存储在进程体中
2 UNIX系统进程通信方式 基本通信:早期的UNIX系统采用,简单的信息传递,协调进程之间的同步和互斥。 管道通信:大批量的数据传送,有名管道和无名管道。 IPC :采用消息方式进行进程间通信。
2 UNIX系统进程通信方式 1.基本通信 • 锁文件通信: 通信双方在某个指定目录中查找是否有个双方约定的文件存在,并以此来决定通信的动作以及对应的逻辑。进程用对“锁文件创建与否”状态的判定和设置完成一个进程到另一个进程之间的通信。 (2) 记录锁定文件通信 即是通过对记录(文件中连续的字节组成特定的数据)的锁定来实现进程通信。 (3)信号 使用软中断信号的通信方式。信号可以做预先的说明,用户使用时只需要根据信号的约定来完成进程之间的通信。
3 信号处理 基本概念 • 信号是进程中异步发生事件时发出的提示信息或传送给进程的一种事件通知,是UNIX操作系统用来通知进程发生了某种事件的一种手段。 • 信号可以由用户进程或核心进程发出,提请系统立即将当前进程中已发生的时间想相关进程进行通告。信号提供了一种处理异步事件的方法。 • 信号也可以用于进程之间进行通信和实现进程同步处理
3 信号处理 很多种情况会产生信号 当用户在终端按下某些键时,产生终端生成的信号,例如: 按下Delete键或者Ctrl-c通常产生一个中断信号 SIGINT,这是停止正在运行的程序的一种常用手段 硬件例外会产生信号,例如: 零作为除数、非法存储器访问等。这种情况通常是由硬件而不是UNIX内核检测到的,但由内核向发生此错误的哪个进程发送相应的信号,比如发生非法存储器访问时,信号SIGSEGV将被内核发送到执行了该非法访问的进程。 如果发生了某种必须让进程知道的情况时也会生成信号。这里的情况不是硬件产生的,而是软条件,例如: 当进程设置的定时器到期时将生成SIGALRM信号 当进程向一个管道写数据,而此管道已不存在读数据方时,生成SIGPIPE信号 某些系统调用将产生信号,例如 kill函数将允许进程发送任何信号给本进程,其他的进程或者进程组 raise函数能够发送任何一个信号给调用它的进程
3 信号处理 生成信号的事件可以归并为三大类 1.程序错误 例如零作除数、非法存储器访问等 2. 外部事件 例如用户按下Delete键、定时器到期等 3. 显式请求 进程主动调用kill函数或者raise函数
3 信号处理 同步信号和异步信号 信号的生成可以是同步的,也可以是异步的。 • 异步信号是接收该信号的进程控制之外的事件生成的信号 • 一般外部事件总是异步的生成信号 • 作用于其他进程的kill系统调用也异步地生成信号 • 异步信号可以在进程运行任意时刻产生,进程无法预期信号到达的时刻 • 同步信号与程序中的某个具体操作相关并且在那个操作进行时同时产生。 • 多数程序错误生成的信号是同步的,例如除零错或内存访问错 • 由进程显示请求而生成的给自己的信号也是同步的,例如raise系统调用
3 信号处理 对信号的处理 无论是同步还是异步信号,信号发生时,系统对信号可以采用3种处理方式 忽略信号 大部分信号都可以被忽略,只有2个除外SIGSTOP和SIGKILL。这两个信号是为了给root用户提供杀掉或停止任何进程的一种手段。 捕获信号 需要告诉UNIX系统内核,当该信号出现时,调用专门提供的一个函数,类似于MFC中的事件函数的概念。这个函数称为信号句柄,或者简称为句柄,它专门对产生信号的事件作出处理。系统调用signal为特定信号设置信号句柄 调用默认动作 系统为每种信号规定了一个默认动作,如果用户进程没有为某个信号设置句柄,则该信号到达时,对该信号的处理由UNIX内核来完成。通常的默认动作有: core dump, 终止进程,忽略信号,进程挂起等几种。
3 信号处理 UNIX系统为每一种可能的事件定义了一组信号,每一个信号有一个信号名,名字均以SIG打头,UNIX系统中常用的信号
3 信号处理 UNIX系统中常用的信号 可以通过man –s5 signal 命令或文件<sys/signal.h>查看UNIX中描述的信号 说明
3 信号处理 信号的生成 信号的产生,可以使用三个系统调用 int raise( int sig ); 功能: 给进程自己发送一个信号 参数:sig:信号标识
3 信号处理 信号的生成 int kill( pid_t pid, int sig ); 功能:发送一个信号给进程或者进程组 参数:pid:进程或进程组id sig:信号标识 • pid>0 将信号传给进程识别码为pid 的进程 • pid=0 将信号传给和目前进程相同进程组的所有进程 • pid=-1 将信号广播传送给系统内所有的进程 • pid<0 将信号传给进程组识别码为pid绝对值的所有进程
3 信号处理 信号的生成 unisigned int alarm( unsigned int seconds ); 功能:在seconds秒后向自己发送一个SIGALRM信号.
3 信号处理 kill函数的实例 #include<unistd.h>#include<signal.h>#include<sys/types.h>#include<sys/wait.h>main(){pid_t pid;int status;if(!(pid=fork())) {printf("Hi I am child process!\n");sleep(100);return;}
3 信号处理 else {printf("send signal to child process (%d)\n",pid);sleep(1);kill(pid ,SIGKILL);wait(&status);if(WIFSIGNALED(status))printf("chile process receive signal %d",WTERMSIG(status)); }}
3 信号处理 子进程的结束状态返回后存于status,底下有几个宏可判别结束情况 WIFEXITED(status):如果子进程正常结束则为非0值。WEXITSTATUS(status):取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。WIFSIGNALED(status):如果子进程是因为信号而结束则此宏值为真WTERMSIG(status):取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED 来判断后才使用此宏。WIFSTOPPED(status):如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。WSTOPSIG(status):取得引发子进程暂停的信号代码,
3 信号处理 alarm函数的实例 #include<unistd.h>#include<signal.h>void handler() {printf(“hello\n”);}main(){int i;signal(SIGALRM,handler);alarm(5);for(i=1;i<7;i++){printf(“sleep %d ...\n”,i);sleep(1);}}
3 信号处理 运行结果
3 信号处理 kill命令: kill的语法格式大致有以下两种方式: kill [-s 信号 | -p ] [ -a ] 进程号 ... kill -l [信号] -s 指定需要送出的信号。既可以是信号名也可以对应数字。 -p 指定kill命令只是显示进程的pid,并不真正送出结束信号。 -l 显示信号名称列表,这也可以在/usr/include/linux/signal.h文件中找到。
3 信号处理 运行结果
3 信号处理 信号的捕获和处理 一般情况下,进程捕获到一个信号后,其默认的操作是终止进程。就如同在进程的运行中临时加入了exit系统调用,对于该进程的执行情况其父进程可以从该竟的返回代码中了解到。但在有些情况下,进程不允许随意被打断。因此对执行中的进程进行信号捕获和社顶特殊的处理是非常必要的。 UNIX中使用系统调用signal接收一种指定类型的信号,并对这种信号做特殊的处理。
3 信号处理 系统调用 signal函数 void* signal(int sig, void(* func)(int) ) ; 参数sig是个整数,指明该系统调用处理哪一个信号 参数func指明信号sig发生时,系统可以采取的3种动作之一: • 常数 SIG_IGN 表示进程运行中接收到指定信号时,采用忽略方式 • 常数 SIG_DFL 恢复对信号的默认处理。也就是说,使用系统自带的处理程序完成信号的处理动作,而不是使用指定函数完成接收信号的处理动作。 • 信号句柄地址 用户自定义的信号处理函数,信号发生时,系统将调用该函数进行处理
3 信号处理 void* signal(int sig, void(* func)(int) )函数的注意事项 • 由signal函数建立的信号句柄应当是一个仅有一个整形参数且没有返回值的函数,其整形参数指明该生成的信号 • 当信号发生时,如果func指向信号句柄,系统在把控制转到信号句柄之前,将首先改变该信号的动作为SIG_DFL • signal函数的返回值是指向信号sig的前一次有效动作的指针,当该函数调用成功,将返回SIG_DFL、SIG_IGN,或者信号句柄地址 • 如果signal调用出错,它返回SIG_ERR,唯一的错误码是EINVAL,即是sig给出的信号数非法
实例1: #include <signal.h> void catch_sig_int( int signo ) { /* signal( SIGINT, catch_sig_int );*/ printf(“SIGINT was caught,user pressed key[delete]\n”); /* signal( SIGINT, catch_sig_int );*/ } void main() { int i = 0; signal( SIGINT, catch_sig_int ); for( i=0; i<5; i++ ) { printf(“ Sleep called #%d\n”, i ); sleep(1); } printf(“exiting..\n”); }
3 信号处理 运行结果
3 信号处理 实例:sig.c #include <signal.h> void signal_handle(int the_signal); void main( void ) { printf(“the process id is %d\n”, getpid()); if ( signal(SIGUSR1, signal_handle ) == SIG_ERR ) err_exit(“can not catch SIGUSR1\n”); if ( signal(SIGUSR2, signal_handle ) == SIG_ERR ) err_exit(“can not catch SIGUSR2\n”); for( ; ; ) pause(); // pause()函数等待信号的到达 }
3 信号处理 void signal_handle(int the_signal) { if ( the_signal == SIGUSR1 ) printf(“the signal is SIGUSR1”); else if ( the_signal == SIGUSR2) printf(“the signal is SIGUSR2”); else printf(“the received signal is %d\n”, the_signal); }
3 信号处理 执行上面源代码编译后的程序,并调度到后台运行 %a.out& the process id is 7346 % kill -s SIGUSR1 7346 发送信号SIGUSR1给进程号为7346的进程 the signal is SIGUSR1 % 后台进程7346收到SIGUSR1信号并调用信号句柄 kill -s SIGUSR2 7346 发送信号SIGUSR2给进程号为7346的进程 the signal is SIGUSR2 % kill 7346 后台进程7346收到SIGUSR2信号并调用信号句柄 [1]+Terminated a.out& % 发送信号SIGTERM给进程号为7346的进程,表示要结束该进程 后台进程7346收到SIGTERM信号,调用默认信号动作,该动作终止进程
3 信号处理 信号操作:有时候我们希望进程正确的执行,而不想进程受到信号的影响,比如我们希望上面那个程序在1秒钟之后不结束.这个时候我们就要进行信号的操作. 信号操作最常用的方法是信号屏蔽,即是进程屏蔽掉某些指定的信号. 信号屏蔽要用到信号集的概念和几个重要的函数. 信号集 该数据结构可以表示系统支持的每一个信号. 在 signal.h中定义了 sigset_t 来描述信号集
3 信号处理 主要的信号操作函数 int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set,int signo); int sigdelset(sigset_t *set,int signo); int sigismember(sigset_t *set,int signo);
3 信号处理 主要的信号操作函数 int sigprocmask(int how,const sigset_t *set,sigset_t *oset); • 功能:改变目前的信号遮罩,其操作依参数how来决定 • SIG_BLOCK 新的信号遮罩由目前的信号遮罩和参数set 指定的信号遮罩作联集 • SIG_UNBLOCK 将目前的信号遮罩删除掉参数set指定的信号遮罩 • SIG_SETMASK 将目前的信号遮罩设成参数set指定的信号遮罩。 • 如果参数oldset不是NULL指针,那么目前的信号遮罩会由此指针返回。 !!注意:信号操作函数 不能对信号SIGKILL和SIGSTOP进行阻塞屏蔽
3 信号处理 以一个实例来解释使用这几个函数. int main() { sigset_t intmask; sigemptyset( &intmask ); // 将信号集合初始化为空 sigaddset( &intmask, SIGINT ); // 加入中断 Ctrl+C 信号 sigaddset( &intmask, SIGTSTP ); // 加入 SIGTSTP信号 sigaddset( &intmask, SIGALRM ); // 加入 SIGTSTP信号 sigaddset( &intmask, SIGTERM ); // 加入 SIGTERM信号 sigprocmask(SIG_BLOCK,&intmask,NULL); //阻塞信号集intmask中的所有信号,不保存原来信号集所以oset为NULL . . . . . . . . /* 进行事务操作,期间进程不会被用户中断,保证事务处理的完成 */ sigprocmask(SIG_UNBLOCK,&intmask,NULL); //放开上面阻塞的信号集 return; }
3 信号处理 等待信号:如果程序是由外部事件所驱动的,或者使用信号进行同步,则需要等待信号的到达. UNIX系统提供函数pause() 和 sigsuspend()来等待信号 int pause(void); 功能:在新的信号到来之前,暂时停止程序的运行 int sigsuspend( sigset_t *sigmask); 功能:sigsuspend函数可以暂时将当前阻塞信号集改变为由sigmask指定的信号集。改变后,sigsuspend会等待,直到一个信号被交付。一旦一个信号被交付后,原先的信号集被恢复。由于sigsuspend调用在信号交付之后总是被终止,它的返回值总是-1,errno总是EINTR
3 信号处理 以一个实例来解释使用这个函数. int main() { . . . . . . // 功能代码 sigset_t mask; sigset_t old_mask sigemptyset(&mask); // 将信号集合初始化为空 sigaddset(&mask, SIGUSR1); // 加入 SIGUSR1 信号 sigprocmask(SIG_BLOCK,&mask, &old_mask); //获取系统当前信号屏蔽 sigsuspend( &old_mask);//等待信号的到达,且是当前信号屏蔽集外的信号 . . . . . . . . sigprocmask(SIG_SETMASK ,&old_mask, NULL ); //恢复系统原来的信号屏蔽 . . . . . . . . return; }
4 管 道 通 信 简介 • 管道(pipe)是UNIX中最古老的进程间通信工具,它提供进程之间单向通信的方法。简单说,管道是连接一个进程的输出到另一个进程的输入的一种方法 • 管道(pipe)的使用很广泛,最常见是在命令行中 %cat file | grep ‘pipe’ | more
4 管 道 通 信 管道 file cat grep more 终端
4 管 道 通 信 实现原理 • UNIX中的管道用于进程通信,是一种先进先出(FIFO)的特殊文件,,通常是一个进程向管道中写入数据,另一个进程从管道中读出数据,从而完成通信的目的。 管道的特点 • 管道单独构成一种特殊的文件。 • 管道中,写入的内容每次都添加在管道的末尾,并且每次都是从管道的头部读出数据,就像队列
4 管 道 通 信 (a)管道写和一般文件写(write) • 不同点是: • 对管道写入时,每次write调用的结果总是附加在管道的末端,而文件的写入不遵守这个规定,它可以通过指针随意移动 • 对管道写入时,每次写入的字节数不能超过系统常量PIPE_BUF,而对文件写入则没有这种规则 • 相同点是: • 当设备处于忙状态时,write调用将被阻塞并被延迟执行 • 当write调用完成时,都能返回实际写入的字节数
4 管 道 通 信 (b)管道读和一般文件读(read) • 对管道读时,所有的read操作总是从管道的当前位置开始,即是管道文件不支持指针的移动,而文件的读不遵守这个规定,它可以通过指针随意移动 • 当管道中没有信息时,对管道进行read操作将被阻塞,直到有数据才返回;而对空文件进行read操作,可以返回空串,并不发生阻塞
4.1 无 名 管 道 1 创建无名管道 为了创建无名管道,需要调用pipe函数,pipe函数建立一个管道,使得两个进程可经由它相互传递信息。我们可以将管道视为一块空间,进程可以经由两个不同的文件描述符来分享这块空间;同时pipe函数产生两个文件描述字,进程通过使用这两个文件描述字来存取管道信息 #include <unistd.h> int pipe( int fdes[2] ); pipe函数的唯一参数是一个由两个整数组成的数组,该函数调用成功后将含有作为管道使用的两个文字描述符,一个作为管道的输入,另一个作为管道的输出。
4.1 无 名 管 道 2 管道读写操作 读操作 int read( int fd, char *buf, int len ); 写操作 int write( int fd, char *buf, int len );
4.1 无 名 管 道 3 管道操作特点 • 用pipe函数创建的管道没有名字,是为了一次使用而创建的 • 管道的两个描述字fdes[0]和fdes[1]是同时打开的,如果从一个没有任何进程写入的管道读,read将返回EOF(文件结束)。如果向一个没有任何进程读取的管道写数据,将产生SIGPIPE信号 • 对有效的管道进行write操作时,如果管道已满,write函数将被阻塞,直到有数据被读出。 • 对有效的管道进行read操作时,如果管道内没有数据,read函数将被阻塞,直到有新数据到达为止。 • 管道内的数据只能被读出1次 • 管道不允许进行文件的定位操作,读和写操作都是顺序的
4.1 无 名 管 道 4 例程 :最简单的管道通信 void main() { int fd[2], pid; char msgsend[] = “Hi! Kid .\n”; char msgrecv[32]; if( pipe( fd ) == -1 ) exit(1); if( (pid=fork()) == 0 ) { //pid ==0 子进程 close( fd[1] ); printf(“before read data from pipe!\n”); read( fd[0], msgrecv, strlen(msgsend)); printf(“read [%s] from pipe\n”, msgrecv); }