赞
踩
一个程序文件 (program) ,只是一堆待执行的代码和部分待处理的数据,他们只有被 加载到内存中,然后让 CPU 逐条执行其代码,根据代码做出相应的动作,才形成一个真正 “活的”、动态的进程 (process) ,因此进程是一个动态变化的过程,是一出有始有终的 戏,而程序文件只是这一系列动作的原始蓝本,是一个静态的剧本。
图 5-1 更好地展示了程序和进程的关系。
当一个程序文件被执行的时候,内核将会产生这么一个结构体,来承载所有该活 动实体日后运行时所需要的所有资源,随着进程的运行,各种资源被分配和释放,是一个动 态的过程。
另外,每个进程都有自己的“身份证号码”,即 PID 号,PID 是重要的系统资源,他是 用以区分各个进程的基本依据。可以使用命令 ps 来查看进程的 PID:
说进程是动态的活动的实体,指的是进程会有很多种运行状态,一会儿睡眠、一会儿暂 停、一会儿又继续执行。下图给出 Linux 进程从被创建 (生) 到被回收 (死) 的全部状态, 以及这些状态发生转换时的条件:
相关重要 API
API(Application Programming Interface)是一组定义了不同软件组件之间交互规则的函数和接口。它提供了一种编程接口,使得开发者可以通过调用这些接口函数来使用底层操作系统或其他软件库提供的功能。
功能
创建一个新的进程
头文件
#include <unistd.h>
原型
pid_t fork(void);
返回值
成功
0 或者大于 0 的正整数
失败
- 1
备注
该函数执行成功之后,将会产生一个新的子进程,在新的子进程中其返 回值为 0 ,在原来的父进程中其返回值为大于 0 的正整数,该正整数就 是子进程的 PID
这个函数接口本身非常简单,简单到连参数都没有,但是这个函数有个非常与众不同的 地方:他会使得进程一分为二!就像细胞分裂一样:
当一个进程调用 fork( )成功后,fork( )将分别返回到两个进程之中,换句话说,fork( ) 在父子两个进程中都会返回,而他们所得到的返回值也不一样,看下图:
1 ,fork( )会使得进程本身被复制 (想想细胞分裂) ,因此被创建出来的子进程和父进 程几乎是一模一样的,说“几乎”意味着子进程并不是 100%为一份父进程的复印件,他们 的具体关系如下:
父子进程的以下属性在创建之初完全一样,子进程相当于搞了一份复制品:
A) 实际 UID 和 GID,以及有效 UID 和 GID。
B) 所有环境变量。
C) 进程组 ID 和会话 ID。
D) 当前工作路径。除非用 chdir()加以修改
E) 打开的文件。
F) 信号响应函数。
G) 整个内存空间,包括栈、堆、数据段、代码段、标准 IO 的缓冲区等等。
而以下属性,父子进程是不一样的:
A) 进程号 PID 。PID 是身份证号码,哪怕亲如父子,也要区分开。
B) 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
C) 挂起的信号。这些信号是所谓的“悬而未决”的信号,等待着进程的响应,子进程 也不会继承这些信号。
2 ,子进程会从 fork( )返回值后的下一条逻辑语句开始运行。这样就避免了不断调用 fork( )而产生无限子孙的悖论。
3,父子进程是相互平等的:他们的执行次序是随机的,或者说他们是并发运行的,除非使用特殊机制来同步他们,否则你不能判断他们的运行究竟谁先谁后。
4,父子进程是相互独立的:由于子进程完整地复制了父进程的内存空间,因此从内存 空间的角度看他们是相互独立、互不影响的。
示列代码如下:
#include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> int main() { printf("hello\n"); pid_t pid = fork(); if(pid == 0) { //sleep(10); printf("[我是son]world,[自己p的pid]%d,[父进程的pid]%d\n",getpid(),getppid());//getpid(),getppid() //给出一个退出值 子进程 父进程 exit(99); //退出码 //正常exit(1),被信号杀死exit(2),异常退出exit(3); } if(pid > 0) { //wait(),等待子进程 int status; wait(&status); printf("[我嫩爹]world,[自己的pid]%d,[父进程的pid]%d\n",getpid(),getppid());// printf("[退出状态值]%d\n",WEXITSTATUS(status));//获取退出值 } return 0; }上述代码中,因为父进程和进程具备并发性 掉用wait函数使父进程先结束子进程再运行。
exit()函数的使用规范
功能
退出本进程
头文件
#include <unistd.h>
#include <stdlib.h>
原型
void _exit(int status);
void exit(int status);
参数
status
子进程的退出值
返回值
不返回
备注
1,如果子进程正常退出,则 status 一般为 0。
2,如果子进程异常退出,则 statuc 一般为非 0。
3,exit( )退出时,会自动冲洗 (flush) 标准 IO 总残留的数据到内核,如果进程注册 了“退出处理函数”还会自动执行这些函数。而_exit( )会直接退出。
表 5-3 函数 exit()和_exit()的接口规范
接下来一个脱口而出的疑问是:好不容易生了个孩子,但是干的事情跟父进程是一样的, 那我们要这个孩子有何用呢?答案是:上述代码确实没有什么实际意义,事实上我们一般会 让孩子去执行一个预先准备好的 ELF 文件或者脚本,用以覆盖从父进程复制过来的代码, 先来介绍这个加载 ELF 文件或者脚本的接口函数:
功能
在进程中加载新的程序文件或者脚本,覆盖原有代码,重新运行
头文件
#include <unistd.h>
原型
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[ ]);
int execle(const char *path, const char *arg, ..., char * const envp[ ]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[ ]);
int execvpe(const char *file, char *const argv[ ],char *const envp[ ]);
参数
path
即将被加载执行的 ELF 文件或脚本的路径
file
即将被加载执行的 ELF 文件或脚本的名字
arg
以列表方式罗列的 ELF 文件或脚本的参数
argv
以数组方式组织的 ELF 文件或脚本的参数
envp
用户自定义的环境变量数组
返回值
成功
不返回
失败
- 1
备注
1,函数名带字母 l 意味着其参数以列表 (list) 的方式提供。
2,函数名带字母 v 意味着其参数以矢量 (vector ) 数组的方式提供。
3,函数名带字母 p 意味着会利用环境变量 PATH 来找寻指定的执行文件。
4,函数名带字母 e 意味着用户提供自定义的环境变量。
表 函数族 exec( )的接口规范
使用exec函数注意事项
1,被加载的文件的参数列表必须以自身名字为开始,以 NULL 为结尾。比如要加载执 行当前目录下的一个叫做 a.out 的文件,需要一个参数”abcd”,那么正确的调用应该是:
execl(“ ./a.out”, “a.out”, “abcd”, NULL);
或者:
const char *argv[3] = {“a.out”, “abcd”, NULL};
execv(“ ./a.out”, argv);
2,exec 函数簇成功执行后,原有的程序代码都将被指定的文件或脚本覆盖,因此这些 函数一旦成功后面的代码是无法执行的,他们也是无法返回的。
示列代码如下:
#include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> int main() { printf("hello\n"); pid_t pid = fork(); if(pid == 0) { execl("./son","./son","1.txt","2.txt","3.txt",NULL);//函数名带l 参数以列表的方式提供 //定义参数数组 // char *arg[] = {"./a.out","1.txt","2.txt","3.txt",NULL}; // execv("./a.out",arg);//函数名带v,将参数列表以数组的方式传入 //execl("/bin/ls","ls",NULL);//execl也可以直接使用shell命令替换子进程的代码,但是需要指定shell命令的路径 //execlp("ls","ls",NULL);//函数名带p,自动寻找环境变量,不需要指定路径 printf("[我是son]world,[自己的pid]%d,[父进程的pid]%d\n",getpid(),getppid()); //getpid(),getppid() //给出一个退出值 exit(99);//exit可以自己指定退出值 } if(pid > 0) { //wait(),等待子进程 int status; wait(&status); printf("[我嫩爹]world,[自己的pid]%d,[父进程的pid]%d\n",getpid(),getppid());// printf("[退出状态值]%d\n",WEXITSTATUS(status)); //status是一个4个字节的状态值,其中最后一个字节才是退出值,用宏WEXITSTATUS()可以直接打印退出值 } return 0; }上述代码注释部分解释了exec的多种用法!
上述程序中使用wait函数如下
功能
等待子进程
头文件
#include <sys/wait.h>
原型
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
参数
pid
小于-1 :等待组 ID 的绝对值为 pid 的进程组中的任一子进程
-1:等待任一子进程
0:等待调用者所在进程组中的任一子进程
大于 0 :等待进程组 ID 为 pid 的子进程
stat_loc
子进程退出状态
option
WCONTINUED:报告任一从暂停态出来且从未报告过的子进程的状态
WNOHANG:非阻塞等待
WUNTRACED:报告任一当前处于暂停态且从未报告过的子进程的状态
返回值
wait( )
成功:退出的子进程 PID
失败:- 1
waitpid( )
成功:状态发生改变的子进程 PID (如果 WNOHANG 被设置,且由 pid 指 定的进程存在但状态尚未发生改变,则返回0) 。
失败:- 1
备注
如果不需要获取子进程的退出状态,stat_loc 可以设置为 NULL
表 5-4 函数 wait()和 waitpid()的接口规范
注意,所谓的退出状态不是退出值,退出状态包括了退出值。如果使用以上两个函数成 功获取了子进程的退出状态,则可以使用以下宏来进一步解析:
宏
含义
①
WIFEXITED(status)
如果子进程正常退出,则该宏为真。
WEXITSTATUS(status)
如果子进程正常退出,则该宏将获取子进程的退出值。
WIFSIGNALED(status)
如果子进程被信号杀死,则该宏为真。
WTERMSIG(status)
如果子进程被信号杀死,则该宏将获取导致他死亡的信号值。
② WCOREDUMP(status)
如果子进程被信号杀死且生成核心转储文件 (core dump) , 则该宏为真。
WIFSTOPPED(status)
如果子进程的被信号暂停,且 option 中 WUNTRACED 已经被 设置时,则该宏为真。
WSTOPSIG(status)
如果 WIFSTOPPED(status)为真,则该宏将获取导致子进程暂 停的信号值。
WIFCONTINUED(status)
如果子进程被信号 SIGCONT 重新置为就绪态,该宏为真。
表 5-5 处理子进程退出状态值的宏
①正常退出指的是调用 exit( )/_exit( ),或者在主函数中调用 return,或者在最后一个 线程调用 pthread_exit( )。
②由于没有在 POSXI.1-2001 标准中定义,这个选项在某些 Unix 系统中无效,比如 AIX, 或者 sunOS 中。
至此,我们已经知道如何创建多进程,以及掌握了他们的基本操作方法了,有一点是必 须再提醒一次的:进程他们是相互独立的,最重要体现在他们互不干扰的内存空间上,他们的数据是不共享的,但如果多个进程需要协同合作,就必然会有数据共享的需求,就像人与 人之间需要说话一样,进程亟需通过某样东西来互相传递信息和数据,这就是所谓的 IPC (Inter-Process Comunication) 机制,IPC 有很多种,他们是怎么使用的,有哪些特点,在 什么场合适用,请关注下一篇文章咱们慢慢道来。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。