进程通信

来源:互联网 发布:宋江 知乎 编辑:程序博客网 时间:2024/06/07 02:47

程序和进程

程序是存放在存储介质上的一个可执行文件,是一些指令的有序集合。进程是操作系统提供的重要功能之一就是进程管理。进程是程序的执行实例,将程序载入内存开始运行之后,才能称之为进程。 进程的状态是变化的,其包括进程的创建、调度和消亡。

进程的状态及转换

从操作系统的角度来看, 进程整个生命周期可以简单划分为三种状态:
( 1) 运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的
进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在
没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
( 2) 就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运
行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程
由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由 I/O 操作完成而进入就
绪状态时,排入高优先级队列。
( 3) 阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求 I/O
而等待 I/O 完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进
程处于阻塞状态。
这里写图片描述
这里写图片描述

进程控制

进程号

每一个进程都会被分配一个唯一的数字编号,PID,类型pid_t。范围:2~32768.当进程被启动时,系统会按顺序选择下一个PID。进程号是唯一的,但可以被重用。当一个进程被终止时就可以重新使用了。Linux中的进程号0,1有内核创建,0进程通常为调度进程或交换进程(swapper)。进程号1是init进程,除调度进程外,在 linux 下面所有的进程都由进程 init 直接或间接创建。

父进程号( PPID):任何进程(除 init 进程)都是由另一个进程创建的,该进程称为
被创建进程的父进程,对应的进程号称为父进程号( PPID)。
进程组号( PGID):进程组是一个或多个进程的集合。他们之间相互关联,进程组可以
接收同一终端的各种信息,关联的进程有一个进程组号( PGID)。

启动新进程

即在一个程序的内部启动另一个程序,从而创建一个新进程。最简单的方式是调用库函数system来完成。
#include "common.h"int main(void){printf("Running ps with system()\n");system("ps");/*启动ps命令*/printf("Done.\n");return 0;}示例: system2.c    #include "common.h"int main(void){printf("Running ps with system()\n");system("ps &");/*启动ps命令,让ps在后台运行,使得程序在启动ps之后可以立即从system调用中返回*/printf("Done.\n");return 0;}

一般来说,使用system函数远非启动其他进程的理想手段,因为它必须要先启动一个shell,再通过shell 启动程序,对 shell 的安装情况及使用环境依赖也很大,所以 system函数的效率并不高。

进程的替换

进程替换是指把当前进程替换为一个新的程序。替换进程使用 exec 系列函数。进程被
exec 替换后,运行中的程序就开始执行由 exec 指定的新的可执行程序中的代码。 新进程
的 PID、 PPID 与原进程完全一样。 并且, exec 一般是不会返回的,因为原进程已经被完全
替换掉了,除非发生错误,出错时, exec 返回-1。

#include <unistd.h>int execl(const char *path, const char *arg, ..., (char*)0);int execlp(const char *file, const char *arg, ..., (char*)0);int execle(const char *path, const char *arg, ..., (char*)0, char * constenvp[]);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execve(const char *file, char *const argv[], char *const envp[]);

6 个函数中只有 execve 是真正意义上的系统调用(内核提供的接口),其他函数都是在此
基础上经过封装的库函数。 记忆方法:
– l(list):参数地址列表,以空指针结尾。
–v(vector):存有各参数地址的指针数组地址,使用时先构造一个指针数组,指针数组
存各参数的地址,然后奖该指针数组作为函数的参数。
–p(path):按系统环境变量 PATH 指定的目录去搜索可执行文件。文件名由第一个参数
file 指定,当指定的文件名中包含/,则将其视为路径名,并直接到指定的路径中执行
程序。
– e(environment):存有环境变量字符串地址的指针数组首地址。 execle 和 execve 改
变的是 exec 启动的程序的环境变量(新的环境变量完全由 environment 指定)。其他
四个函数启动的程序则使用默认系统环境变量。
可能的调用形式:

char *const ps_argv[] = {"ps", "ax", 0};char *const ps_envp[] = {"PATH=/bin:/usr/bin", 0};execl("/bin/ps", "ps", "ax", 0);execlp("ps", "ps", "ax", 0);execle("/bin/ps", "ps", "ax", 0, ps_envp);execv("/bin/ps", ps_argv);execvp("ps", ps_argv);execve("/bin/ps", ps_argv, ps_envp);

int main(void){printf("Running ps with execlp\n");execlp("ps", "ps", 0); /*此处用ps替换当前进程,后面的Done不会打印*/printf("Done.\n");return 0;}

进程的复制

以上两种方法创建新进程都有一定的限制,要在程序中创建一个完全分离的进程,还可
以使用复制进程的方式,使用 fork 系统调用来完成。 fork 系统调用复制当前进程,在进程
表中创建一个新的表项,新表项中许多属性与当前进程是相同的。新进程几乎与原进程一模
一样,执行的代码也完全相同,但新进程有自己的数据空间、环境和文件描述符。 fork 和
exec 系列函数结合在一起使用就是创建新进程所需要的一切了。

#include <sys/types.h>#include<unistd.h>
pid_t fork(void);

功能:
fork()函数用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程
称为父进程。 子进程从 fork 之后的代码开始执行。
返回值:
成功: 子进程中返回 0,父进程中返回子进程 ID。
失败:返回-1。
使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。
地址空间:包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进
程优先级、进程组号等。
子进程所独有的只有它的进程号,计时器等,因些,使用 fork 函数的代价很大。

示例: fork1.c

int main(void){pid_t pid;pid = fork();if(pid < 0)err_sys("fork error");else if(pid == 0){printf("child process\n");printf("child pid:%d\n", getpid());printf("child ppid:%d\n", getppid());}else{printf("parent process\n");printf("parent pid:%d\n", getpid());}return 0;}

程序在调用 fork 时被分为两个独立的进程,程序通过 fork 调用的返回值判断父子进程。

示例: fork2.c

int global = 9;int main(void){int num = 10;pid_t pid;pid = fork();if(pid < 0)err_sys("fork error");else if(pid == 0){global++;num++;printf("in child process global=%d, num=%d\n", global, num);}else{sleep(1);printf("in parent process global=%d, num=%d\n", global, num);}printf("Done.\n");return 0;}

从以上代码执行的结果可以看出,子进程对变量所做的改变并不影响父进程中该变量的值,说明父子进程各有自拥有自己的地址空间
示例: fork3.c

#include "common.h"#include <sys/stat.h>#include <fcntl.h>int main(void){pid_t pid;int fd = open("fork.data", O_RDWR|O_CREAT|O_TRUNC, 0755);if(fd < 0)err_sys("open fail");pid = fork();if(pid < 0)err_sys("fork error");else if(pid == 0){write(fd, "hello", strlen("hello\n"));}else{sleep(1);write(fd, "world\n", strlen("world\n"));}printf("Done.\n");return 0;}

调用fork函数之后,父进程打开的文件描述符都被复制到子进程,如果父进程父进程重定向文件描述符,则子进程也会被重定向。
fork的用法:
1.一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求的到达。
2.一个进程要执行一个不同的程序,这对Shell是常见的情况。这种情况下,子进程从fork返回后立即调用exec。

vfork函数

一般来说,在 fork 之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。如果要求父子进程之间相互同步,则要求某种形式的进程间通信。或是使用 vfork。
fork 和 vfork 的区别:
vfork 保证子进程先运行,在它调用 exec 或 exit 之后,父进程才可能被调度运行。
* vfork 和 fork 一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,而是和父进程共享内存数据,因为子进程会立即调用 exec 或 exit,于是也就不访问该地址空间。相反,在子进程中调用exec 或 exit 之前,它在父进程的地址空间中运行,在exec 之后子进程才会有自己的进程空间。
示例: vfork1.c

int global = 9;int main(void){    int num = 10;    pid_t pid;    pid = vfork();    if(pid < 0)        err_sys("fork error");    else if(pid == 0)    {        global++;        num++;        printf("in child process global=%d, num=%d\n", global, num);        exit(0);    }    else    {        printf("in parent process global=%d, num=%d\n", global, num);    }    printf("Done.\n");    return 0;}

进程的挂起

进程在一定时间没有任何动作,称为进程的挂起。
功能:挂起进程指定的秒数,直到指定的时间用完或是收到信号才解除挂起。
返回值:若进程挂起到直到指定的秒数则返回0,或有信号中断则返回剩余的秒数。
注意:进程挂起指定的秒数或并不会立即执行,系统只是将进程切换到就绪。

进程的等待

当 fork 启动一个子进程时,子进程就有了它自己的生命周期并将独立运行。而有时父
子进程需要简单的进程间同步,比如父进程等待子进程结束。我们可以通过在父进程中调用
wait 和 waitpid 函数让父进程等待子进程的结束。
pid_t wait(int *status);
功能:
等待子进程结束,如果子进程结束,此函数会回收子进程的资源。子进程未结束时,调
用 wait 函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被
唤醒。若调用进程没有子进程或它的子进程已经结束,则该函数立即返回。
参数:
函数返回时,参数 status 中包含子进程退出时的状态信息。子进程的退出信息在一个
int 类型中包含了多个字段,可以用相关的宏取出其中每个字段的含义:
 WIFEXITED(status) 如果子进程是正常结束,取出的字段是非零值。
 WEXITSTATUS(status) 如果 WIFEXITED 非零,它返回子进程的退出码。
 WIFSTOPPED(status) 如果子进程意外终止,取出的字段是非零值。
返回值:
执行成功返回子进程的进程号,出错返回-1。
示例: wait1.c

int main(void){pid_t pid;pid = fork();if(pid < 0)err_sys("fork error");else if(pid == 0){sleep(1);printf("child process\n");printf("child pid:%d\n", getpid());printf("child ppid:%d\n", getppid());exit(100);//abort();}else{printf("parent process\n");printf("parent pid:%d\n", getpid());int status;pid_t child_pid;child_pid = wait(&status);printf("child process finished: PID = %d\n", child_pid);if(WIFEXITED(status) != 0){printf("child process exited with code %d\n",WEXITSTATUS(status));}elseprintf("child process exited abnormally\n");}return 0;}

pid_t waitpid(pid_t pid, int *status, int options);
功能:
等待子进程终止,如果子进程终止,此函数会回收子进程的资源。
参数:
参数 pid 的值有以下几种类型:
 pid > 0:等待进程 ID 等于 pid 的子进程。
 pid = 0:等待同一个进程组中的任何子进程,如果子进程已经加入别的进程组,
waitpid 不会等待它。
 pid = -1:等待任一子进程,此时 waitpid 和 wait 作用一样。
 pid < -1:等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对
值。
参数 status 和 wait 中的选项相同。
参数 options 能进一步控制 waitpid 的操作:
 0:同 wait,阻塞父进程,等待子进程退出。
 WNOHANG:没有任何子进程结束时将不阻塞父进程, waitpid 调用立即返回,可用
于检测是否有子进程结束
 WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予理会子进程的结束状
态。(跟踪调用,很少用到)
返回值:
成功返回改变了的子进程 ID,如果设置了选项 WNOHANG 并且 pid 指定的进程存在且未
结束则返回 0。出错返回-1,当 pid 所指示的子进程不存在,或此进程存在,但不是调用进程的子进程时, waitpid 就会出错返回,这时 errno 被设置为 ECHILD。

进程的退出

在 linux 下可以通过 exit 和_exit 结束正在运行的进程
参数 status 是返回给父进程的退出码(低 8 位有效)。exit 和_exit 的区别: exit 为库函数, _exit 为系统调用。在进程退出时可以用 atexit 函数注册退出处理函数。
int atexit(void (*function)(void));
功能:
注册进程正常结束前调用的函数。
参数:
function:进程结束前,调用函数的入口地址。
一个进程中可以多次调用 atexit 函数注册清理函数,正常结束前调用函数的顺序和注册时的顺序相反
示例: atexit1.c

void clear_func1(void){printf("perform clear func 1\n");}void clear_func2(void){printf("perform clear func 2\n");}void clear_func3(void){printf("perform clear func 3\n");}int main(void){atexit(clear_func1);atexit(clear_func2);atexit(clear_func3);printf("process exit 3 sec later.\n");sleep(3);return 0;}

僵尸进程

因为父进程的异常退出,使得子进程成为一个僵尸进程。并被init进程接管

孤儿进程&守护进程

孤儿进程( Orphan Process):父进程运行结束,但子进程未运行结束的子进程。
守护进程( Daemon Process):守护进程是个特殊的孤儿进程,这种进程脱离终端,在后台运
行。

1 0
原创粉丝点击