Linux 下多进程编程详解

一.多进程程序的特点 进程是一个具有独立功能的程序关于某个数据集合的一次可以并发执行的运行活动,是处 于活动状态的计算机程序。 进程作为构成系统的基本细胞, 不仅是系统内部独立运行的实体, 而且是独立竞争资源的基本实体。 进程是资源管理的最小单位,线程是程序执行的最小单位。进程管理着资源(比 如 cpu、内存、文件等等),而将线程分配到某个 cpu 上执行。在操作系统设计 上,从进程演化出线程,最主要的目的就是更好的支持多处理器系统和减小上下 文切换开销。 进程的状态 系统为了充分的利用资源,对进程区分了不同的状态.将进程分为新 建,运行,阻塞,就绪和完成五个状态. 新建 表示进程正在被创建, 运行 是进程正在运行, 阻塞 是进程正在等待某一个事件发生, 就绪 是表示系统正在等待 CPU 来执行命令, 完成 表示进程已经结束了系统正在回收资源. 由于 UNIX 系统是分时多用户系统, CPU 按时间片分配给各个用户使用,而在实质上应 该说 CPU 按时间片分配给各个进程使用, 每个进程都有自己的运行环境以使得在 CPU 做进 程切换时不会”忘记”该进程已计算了一半的”半成品”. 以 DOS 的概念来说, 进程的切换都 是一次”DOS 中断”处理过程, 包括三个层次: 1) 用户数据的保存: 包括正文段(TEXT), 数据段(DATA,BSS), 栈段(STACK), 共享内 存段(SHARED MEMORY)的保存. 2) 寄存器数据的保存: 包括 PC(program counter,指向下一条要执行的指 令的地址), PSW(processor status word,处理机状态字), SP(stack pointer,栈指针), PCBP(pointer of process control block,进程控制块指针), FP(frame pointer,指向栈中一个函数的 local 变量的首地址), AP(augument pointer,指向栈中函数调用的实参位置), ISP(interrupt stack pointer,中断栈指针), 以及其他的通用寄存器等. 3) 系统层次的保存: 包括 proc,u,虚拟存储空间管理表格,中断处理栈.以便于该进程再一次得到 CPU 时 间片时能正常运行。 既然系统已经处理好所有这些中断处理的过程, 我们做程序 还有什么要担心 的呢? 我们尽可以使用系统提供的多进程的特点, 让几个程序精 诚合作, 简单而又高效地把结果给它搞出来。 多进程程序的 一些突出的特点: 并行化 简单有序 互不干扰 事务化 二.常用的多进程编程的系统调用 1)功能:创建一个新的进程. 语法: #include <unistd.h> #include <sys/types.h> pid_t fork(); 说明:本系统调用产生一个新的进程, 叫子进程, 是调用进程的一个复制品. 调用进程叫父进 程, 子进程继承了父进程的几乎所有的属性。 简述:fork() 调用成功时,分别返回两个整数,对父进程返回 〉0 的整数,对子进程返回 0, 进程:代码段(程序代码) 堆栈段(局部变量、函数返回地址、函数参数) 数据段(全局变量、常数等) 在 Linux 系统中,系统调用 fork 后,内核为完成系统调用 fork 要进行几步操作: 第一步,为新进程在进程表中分配一个表项。系统对一个普通用户可以同时运行的进 程数是有限制的,对超级用户没有该限制,但不能超过进程表的最大表项的 数目。 第二步,给子进程一个唯一的进程标识号(PID) 第三步,复制一个父进程的进程表项的副本给子进程。内核初始化子进程的进程表项 时,是从父进程处拷贝的。所以子进程拥有与父进程一样的 uid、当前目录、 当前根、用户文件描述符表等。 第四步, 父进程相连的文件表和索引节点表的引用数加 1。 这些文件自动地与该子 进程相连。 第五步,内核为子进程创建用户级上下文。内核为子进程的代码段分配内存,并复制 父进程的区内容,生成的是进程的静态部分。 第六步,生成进程的动态部分,然后对父进程返回子进程的 pid,对子进程返回 0。 从父进程拷贝的内容主要有: 用户标识符,包括实际用户号(real)和有效用户号(effective) 环境变量 打开的文件描述符、套接字描述符 信号处理设置 堆栈 目录 进程组标志(process ID) 会晤组标志(session ID) 正文 子进程特有内容: 进程号 父进程号 进程执行时间 未处理的信号被处理为空 不继承异步的输入输出操作 函数执行过程: 1 内核在系统进程表中,创建一个新条目; 2 复制父进程内容(已打开的文件描述符、堆栈、正文等) 3 修改两者的堆栈,给父进程返回子进程号,给子进程返回 0(父进程知道每个子进程 的标志号,而子进程可根据需要调用 getppid() 来获得父进程的标志号) 例子: #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc,char *argv[]) { pid_t pid; if((pid=fork())==0)//对子进程返回==0的整数 { printf(“This is Child fork\n”);//子进程代码 printf(“Child pid= %d\n”,pid);//子进程代码 //exit(0); } else if(pid>0) //对父进程返回 〉0 的整数 { printf(“This is Father fork\n”);//父进程代码 printf(“Father pid= %d\n”,pid);//子进程代码 //exit(0); } else { printf(“Error”); exit(1); } } $gcc -o fork fork.c $./fork This is Father fork Father pid= 6250 This is Child fork Child pid= 0 2)system() 应用程序 fork ( ) 父进程 子进程 1 子进程 2 子进程执行指定的命令 功能:产生一个新的进程, 子进程执行指定的命令. 语法: #include <stdio.h> #include <stdlib.h> int system(string) char *string; 说明: 本调用将参数 string 传递给一个命令解释器(一般为 sh)执行, 即 string 被解释为一条命 令, 由 sh 执行该命令.若参数 string 为一个空指针则为检查命令解释器是否存在. 该命令可以同命令行命令相同形式, 但由于命令做为一个参数放在系统调用中, 应注意 编译时对特殊意义字符的处理. 命令的查找是按 PATH 环境变量的定义的. 命令所生成的后 果一般不会对父进程造成影响. 返回值:当参数为空指针时, 只有当命令解释器有效时返回值为非零. 若参数不为空指 针, 返回值为该命令的返回状态(同 waitpid())的返回值. 命令无效或语法错误则返回非零值, 所执行的命令被终止. 其他情况则返回-1. 例子: #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc,char *argv[]) { char command[81]; sprintf(command,”ps aux”); system(command); } 3)exec() 功能:执行一个文件 语法 #include <unistd.h> int execve(const char* path, char* const* argv,char* const* envp); int execl(const char* path, char* arg,…); int execp(const char* file, char* arg,…); int execle(const char* path, const char* argv,…,char* const* envp); int execv(const char* path, char* const* arg); int execvp(const char* file, char* const* arg); 说明: exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内 容,换句话说,就是在调用进程内部执行一个可执行文件 其中只有 execve 是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。 与一般情况不同,exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包 括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍 保持原样,颇有些神似”三十六计”中的”金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新 的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。 fork()和 exec()这两个函数,前者用于并行执行,父、子进程执行相同正文中的不同 部分;后者用于调用其他进程,父、子进程执行不同的正文,调用前,一般应为子进程创造 一个干净的环境。 fork()以后,父、子进程共享代码段,并只重新创建数据有改变的页(段页式管理) exec()以后,建立新的代码段,用被调用程序的内容填充。 前者的子进程执行后续的公共代码,后者的子进程不执行后续的公共代码。 父、子进程以及各个子进程执行的顺序不定。 例子:printf(“now this process will be ps command\n”); execl(“/bin/ps”,”ps”,”-ef”,NULL); 4)popen() 功能:初始化从/到一个进程的管道. 语法: #include <stdio.h> FILE *popen(command,type) char *command,type; 说明:本系统调用在调用进程和被执行命令间创建一个管道. 参数 command 做为被执行的命令行.type 做为 I/O 模式,”r”为从被 执行命令读,”w”为向被执行命令写.返回一个标准流指针,做为管 道描述符,向被执行命令读或写数据(做为被执行命令的 STDIN 或 STDOUT)该系统调用可以用来在程序中调用系统命令,并取得命令 的输出信息或者向命令输入信息. 返回值:不成功则返回 NULL,成功则返回管道的文件指针. 5)pclose() 功能:关闭到一个进程的管道. 语法: #include <stdio.h> int pclose(strm) FILE *strm; 说明:本系统调用用于关闭由 popen()打开的管道,并会等待由 popen() 激活的命令执行结束后,关闭管道后读取命令返回码. 返回值:若关闭的文件描述符不是由 popen()打开的,则返回-1. 例子: printf(“now this process will call popen system call\n”); FILE * fd; if ((fd=popen(“ps -ef”,”r”))==NULL) { printf(“call popen failed\n”); return; } else { char str[80]; while (fgets(str,80,fd)!=NULL) printf(“%s\n”,str); } pclose(fd); 6)wait() 功能:等待一个子进程返回并修改状态 语法: #include <sys/types.h> #include <sys/wait.h> pid_t wait(stat_loc) int *stat_loc; 说明:允许调用进程取得子进程的状态信息.调用进程将会挂起直到其 一个子进程终止. 返回值:等待到一个子进程返回时,返回值为该子进程号,否则返回值为 -1.stat_loc返回子进程的返回值. 例子:/*父进程*/ if (fork()>0) { wait((int *)0); /*父进程等待子进程的返回*/ } else { /*子进程处理过程*/ exit(0); } 7)waitpid() 功能:等待指定进程号的子进程的返回并修改状态 语法: #include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid,stat_loc,options) pid_t pid; int *stat_loc,options; 说明:当 pid 等于-1,options 等于 0 时,该系统调用等同于 wait().否则该 系统调用的行为由参数 pid 和 options 决定. pid 指定了一组父进程要求知道其状态的子进程: -1:要求知道任何一个子进程的返回状态. >0:要求知道进程号为 pid 值的子进程的状态. <-1:要求知道进程组号为 pid 的绝对值的子进程的状态. options 参数为以比特方式表示的标志以或运算组成的位图,每个 标志以字节中某个比特置 1 表示: WUNTRACED:报告任何未知而又已停止运行的指定进程号的子进 程的状态.该子进程的状态自停止运行时起就没有被报告 过. WCONTINUED:报告任何继续运行的指定进程号的子进程的状态, 该子进程的状态自继续运行起就没有被报告过. WHOHANG:若调用本系统调用时,指定进程号的子进程的状态目 前并不是立即有效的(即可被立即读取的),调用进程并被 暂停执行. WNOWAIT:保持将其状态设置在 stat_loc 的进程在可等待状态. 该进程将等待直到下次被要求其返回状态值. 返回值:等待到一个子进程返回时,返回值为该子进程号,否则返回值为 1. 同时 stat_loc 返回子进程的返回值. 例子:pid_t pid; int stat_loc; /*父进程*/ if ((pid=”fork())”>0) { waitpid(pid,&stat_loc,0); /*父进程等待进程号为 pid 的子进程的返回*/ } else { /*子进程的处理过程*/ exit(1); } /*父进程*/ printf(“stat_loc is [%d]\n”,stat_loc); /*字符串”stat_loc is [1]”将被打印出来*/ 设置进程组号和会话号 2.8.setpgrp() 功能:设置进程组号和会话号. 语法: #include <sys/types.h> pid_t setpgrp() 说明:若调用进程不是会话首进程.将进程组号和会话号都设置为与它 的进程号相等.并释放调用进程的控制终端. 返回值:调用成功后,返回新的进程组号. 例子:/*父进程处理*/ if (fork()>0) { /*父进程处理*/ } else { setpgrp(); /*子进程的进程组号已修改成与它的进程号相同*/ exit(0); } 2.9.exit() 终止进程 功能:终止进程. 语法: #include <stdlib.h> void exit(status) int status; 说明:调用进程被该系统调用终止.引起附加的处理在进程被终止前全 部结束. 返回值:无 2.10.signal() 信号管理功能 功能:信号管理功能 语法: #include <signal.h> void (*signal(sig,disp))(int) int sig; void (*disp)(int); void (*sigset(sig,disp))(int) int sig; void (*disp)(int); int sighold(sig) int sig; int sigrelse(sig) int sig; int sigignore(sig) int sig; int sigpause(sig) int sig; 说明:这些系统调用提供了应用程序对指定信号的简单的信号处理. signal()和 sigset()用于修改信号定位.参数 sig 指定信号(除了 SIGKILL 和 SIGSTOP,这两种信号由系统处理,用户程序不能捕捉到). disp 指定新的信号定位,即新的信号处理函数指针.可以为 SIG_IGN,SIG_DFL 或信号句柄地址. 若使用 signal(),disp 是信号句柄地址,sig 不能为 SIGILL,SIGTRAP 或 SIGPWR,收到该信号时,系统首先将重置 sig 的信号句柄为 SIG_DFL, 然后执行信号句柄. 若使用 sigset(),disp 是信号句柄地址,该信号时,系统首先将该 信号加入调用进程的信号掩码中,然后执行信号句柄.当信号句柄 运行结束 后,系统将恢复调用进程的信号掩码为信号收到前的状态.另外, 使用 sigset()时,disp 为 SIG_HOLD,则该信号将会加入调用进程的 信号掩码中而信号的定位不变. sighold()将信号加入调用进程的信号掩码中. sigrelse()将信号从调用进程的信号掩码中删除. sigignore()将信号的定位设置为 SIG_IGN. sigpause()将信号从调用进程的信号掩码中删除,同时挂起调用 进程直到收到信号. 若信号 SIGCHLD 的信号定位为 SIG_IGN,则调用进程的子进程在终 止时不会变成僵死进程.调用进程也不用等待子进程返回并做相 应处理. 返回值:调用成功则 signal()返回最近调用 signal()设置的 disp 的值. 否则返回 SIG_ERR. 例子一:设置用户自己的信号中断处理函数,以 SIGINT 信号为例: int flag=0; void myself() { flag=1; printf(“get signal SIGINT\n”); /*若要重新设置 SIGINT 信号中断处理函数为本函数则执行以 *下步骤*/ void (*a)(); a=myself; signal(SIGINT,a); flag=2; } main() { while (1) { sleep(2000); /*等待中断信号*/ if (flag==1) { printf(“skip system call sleep\n”); exit(0); } if (flag==2) { printf(“skip system call sleep\n”); printf(“waiting for next signal\n”); } } } 2.11.kill() 向一个或一组进程发送一个信号 功能:向一个或一组进程发送一个信号. 语法: #include <sys/types.h> #include <signal.h> int kill(pid,sig); pid_t pid; int sig; 说明:本系统调用向一个或一组进程发送一个信号,该信号由参数 sig 指 定,为系统给出的信号表中的一个.若为 0(空信号)则检查错误但 实际上并没有发送信号,用于检查 pid 的有效性. pid 指定将要被发送信号的进程或进程组.pid 若大于 0,则信号将 被发送到进程号等于 pid 的进程;若 pid 等于 0 则信号将被发送到所 有的与发送信号进程同在一个进程组的进程(系统的特殊进程除 外);若 pid 小于-1,则信号将被发送到所有进程组号与 pid 绝对值 相同的进程;若 pid 等于-1,则信号将被发送到所有的进程(特殊系 统进程除外). 信号要发送到指定的进程,首先调用进程必须有对该进程发送信 号的权限.若调用进程有合适的优先级则具备有权限.若调用进程 的实际或有效的 UID 等于接收信号的进程的实际 UID 或用 setuid() 系统调用设置的 UID,或 sig 等于 SIGCONT 同时收发双方进程的会话 号相同,则调用进程也有发送信号的权限. 若进程有发送信号到 pid 指定的任何一个进程的权限则调用成功, 否则调用失败,没有信号发出. 返回值:调用成功则返回 0,否则返回-1. 例子:假设前一个例子进程号为 324,现向它发一个 SIGINT 信号,让它做 信号处理: kill((pid_t)324,SIGINT); 2.12.alarm() 设置一个进程的超时时钟 功能:设置一个进程的超时时钟. 语法: #include <unistd.h< unsigned int alarm(sec) unsigned int sec; 说明:指示调用进程的超时时钟在指定的时间后向调用进程发送一个 SIGALRM 信号.设置超时时钟时时间值不会被放入堆栈中,后一次 设置会把前一次(还未到超时时间)冲掉. 若 sec 为 0,则取消任何以前设置的超时时钟. fork()会将新进程的超时时钟初始化为 0.而当一个进程用 exec() 族系统调用新的执行文件时,调用前设置的超时时钟在调用后仍 有效. 返回值:返回上次设置超时时钟后到调用时还剩余的时间秒数. 例子:int flag=0; void myself() { flag=1; printf(“get signal SIGALRM\n”); /*若要重新设置 SIGALRM 信号中断处理函数为本函数则执行 *以下步骤*/ void (*a)(); a=myself; signal(SIGALRM,a); flag=2; } main() { alarm(100); /*100 秒后发超时中断信号*/ while (1) { sleep(2000); /*等待中断信号*/ if (flag==1) { printf(“skip system call sleep\n”); exit(0); } if (flag==2) { printf(“skip system call sleep\n”); printf(“waiting for next signal\n”); } } } 2.13.msgsnd() 发送消息到指定的消息队列中 功能:发送消息到指定的消息队列中. 语法: #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd(msqid,msgp,msgsz,msgflg) int msqid; void *msgp; size_t msgsz; int msgflg; 说明:发送一个消息到由 msqid 指定消息队列标识号的消息队列. 参数 msgp 指向一个用户定义的缓冲区,并且缓冲区的第一个域应 为长整型,指定消息类型,其他数据放在缓冲区的消息中其他正文 区内.下面是消息元素定义: long mtype; char mtext[]; mtype 是一个整数,用于接收进程选择消息类型. mtext 是一个长度为 msgsz 字节的任何正文,参数 msgsz 可从 0 到系 统允许的最大值间变化. msgflg 指定操作行为: . 若(msgflg&IPC_NOWAIT)是真的,消息并不是被立即发送而调用 进程会立即返回. . 若(msgflg&IPC_NOWAIT)不是真的,则调用进程会被挂起直到下 面情况之一发生: * 消息被发送出去. * 消息队列标志被系统删除.系统调用返回-1. * 调用进程接收到一个未被忽略的中断信号,调用进程继续 执行或被终止. 调用成功后,对应指定的消息队列的相关结构做如下动作: . 消息数(msg_qnum)加 1. . 消息队列最近发送进程号(msg_lspid)改为调用进程号. . 消息队列发送时间(msg_stime)改为当前系统时间. 以上信息可用命令 ipcs -a 看到. 返回值:成功则返回 0,否则返回-1. 2.14.msgrcv() 从消息队列中取得指定类型的消息 功能:从消息队列中取得指定类型的消息. 语法: #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgrcv(msqid,msgp,msgsz,msgtyp,msgflg) int msqid; void *msgp; int msgsz; long msgtyp; int msgflg; 说明:本系统调用从由 msqid 指定的消息队列中读取一个由 msgtyp 指定 类型的消息到由 msgp 指向的缓冲区中,同样的,该缓冲区的结构如 前所述,包括消息类型和消息正文.msgsz 为可接收的消息正文的 字节数.若接收到的消息正文的长度大于 msgsz,则会被截短到 msgsz 字节为止(当消息标志 msgflg&MSG_NOERROR 为真时),截掉的 部份将被丢失,而且不通知消息发送进程. msgtyp 指定消息类型: . 为 0 则接收消息队列中第一个消息. . 大于 0 则接收消息队列中第一个类型为 msgtyp 的消息. . 小于 0 则接收消息队列中第一个类型值不小于 msgtyp 绝对值且 类型值又最小的消息. msgflg 指定操作行为: . 若(msgflg&IPC_NOWAIT)是真的,调用进程会立即返回,若没有 接收到消息则返回值为-1,errno 设置为 ENOMSG. . 若(msgflg&IPC_NOWAIT)不是真的,则调用进程会被挂起直到下 面情况之一发生: * 队列中的消息的类型是有效的. * 消息队列标志被系统删除.系统调用返回-1. * 调用进程接收到一个未被忽略的中断信号,调用进程继续 执行或被终止. 调用成功后,对应指定的消息队列的相关结构做如下动作: . 消息数(msg_qnum)减 1. . 消息队列最近接收进程号(msg_lrpid)改为调用进程号. . 消息队列接收时间(msg_rtime)改为当前系统时间. 以上信息可用命令 ipcs -a 看到. 返回值:调用成功则返回值等于接收到实际消息正文的字节数. 不成功则返回-1.
<

免责声明:文章内容来自互联网,本站不对其真实性负责,也不承担任何法律责任,如有侵权等情况,请与本站联系删除。
转载请注明出处:Linux 下多进程编程详解 https://www.yhzz.com.cn/a/15185.html

上一篇 2023-05-12 15:43:00
下一篇 2023-05-12 15:47:00

相关推荐

联系云恒

在线留言: 我要留言
客服热线:400-600-0310
工作时间:周一至周六,08:30-17:30,节假日休息。