目录
多进程
(一)定义
1、进程
- 当用户敲入命令执行一个程序的时候,对系统而言,它将启动一个进程。
- linux下 一个进程在内存里有三部分的数据,就是"代码段"、“堆栈段"和"数据段”。
- init 进程
(1)Linux下任何程序的第一个进程都是 init 进程,init是Linux系统操作中不可缺少的程序之一。所谓的init进程,它是一个由内核启动的用户级进程。内核自行启动之后,就通过启动一个用户级程序init的方式,完成引导进程。所以,init始终是第一个进程,其进程编号始终为1。
(2)内核会在过去曾使用过init的几个地方查找它,它的正确位置(对Linux系统来说)是/sbin/init。如果内核找不到init,它就会试着运行/bin/sh,如果运行失败,系统的启动也会失败。 - 每个进程有4G独立的进程空间,其中0-3G是用户空间,3G-4G是内核空间。
- 命令
(1)netstat -anp | grep :查找进程监听的端口情况以及进程号。
(2)ps 相关命令:报告当前系统的进程状态。可以搭配kill指令随时中断、删除不必要的程序。
ps aux : 显示系统所有的进程
ps -elf : 显示系统所有的进程(通用)
ps -aux | grepaa
:查看指定(aa)进程
(3)top :动态查看进程信息
(4)pstree: 查看父子关系结构的进程
(5)kill -9 进程号 :杀死进程。(通过信号) - 进程状态:进程是程序的执行过程,根据他的生命周期可以分为3种状态。
执行态: 该进程正在运行,即进程正在使用CPU
就绪态: 进程已经具备执行的一切条件,正在等待分配CPU的处理
等待态: 进程不能使用CPU,若等待事件发生(等待的资源分配到),将其唤醒
还有引入创建态和终止态构成了进程的五态模型。
2、多进程
- 在一个进程中,系统可能需要完成多个独立的任务,这时一个进程显然无法满足要求,就需要创建多个进程。
- 因为多个进程是在独立完成自己的任务,所以看起来这些进程是并行的,是实际上,对于一个单核CPU来讲,从宏观上是并行的,而从微观上是串行的,它使用时间片划分周期调用来实现,每个任务在一段时间内会分到一段时间片(占cpu的时间),在这段时间内该任务只能运行时间片长度,每个任务执行一点每个任务执行一点,从而达到“同时”的效果。
- Linux启动时,0进程启动1号进程(init )和2号进程(内核线程), 0号进程退出, 其它进程是由1、2直接或间接产生。
1号进程(init ) 是所有用户进程的祖先。
2号进程(内核线程) 是内核进程的祖先。
(二)进程相关API
1、创建
(1)fork()
- 头文件:
#include <unistd.h>
#include<sys/types.h>
- 函数原型:
pid_t fork( void);(pid_t 是一个宏定义,其实质是int 被定义在#includesys/types.h>中)
-
函数说明:现有进程创建新进程。由fork创建的新进程被称为子进程(child process)。
-
返回值: 成功:子进程中返回 0,父进程中返回子进程 ID。失败:返回 -1。
-
fork 函数调用失败的原因主要有两个:
- 系统中已经有太多的进程;
- 该实际用户 ID 的进程总数超过了系统限制。
-
系统在创建新的子进程成功后,会将 父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有自己独立的空间,子进程对这些内存的修改并不会影响父进程 空间的相应内存。这时系统中出现两个基本完全相同的进程(父、子进程),这两个进程执行没有固定的先后顺序,哪个进程先执 行要看系统的进程调度策略。如果需要确保让父进程或子进程先执行,则需要程序员在代码中通过进程间通信的机制来自己实 现。
-
经典例题:
问题:不算 main 这个进程,下列代码一共创建了多少个进程?
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
fork();
fork() &&fork() || fork();
fork();
return 0;
}
答案是:19个
具体详解这里不做赘述,可移步:fork()&&fork()||fork(),讲的很详细
(2) vfork()
- 头文件:
#include <sys/types.h>
#include <unistd.h>
- 函数原型:
pid_t vfork(void);(pid_t 是一个宏定义,其实质是int 被定义在#includesys/types.h>中)
- 函数说明:在已有的进程中创建一个新的进程。
- 返回值: 成功:子进程中返回 0,父进程中返回子进程 ID。失败:返回 -1。
- vfork ()保证子进程先运行,在子进程调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
- fork 与 vfork 的区别
- 子进程继承父进程的东西不一样:
fork ():子进程拷贝父进程的数据段,代码段,fork() 会将 父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有自己独立的空间
vfork():子进程与父进程共享数据段,vfork()并不将父进程的地址空间完全复制到子进程中 - 父子进程执行次序不同
fork():子进程的执行次序不确定
vfork():保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在子进程调用 exec 或 exit 之后父进程才可能被调度运行
- 子进程继承父进程的东西不一样:
2、执行
(1) 获取进程ID
1)getppid()
- 头文件:
#include <sys/types.h>
#include <unistd.h>
- 函数原型:
pid_t getppid(void);
- 函数说明:获取自己的父进程的ID。
- 返回值: 成功:本进程的父进程的ID号。失败:返回 -1。
2) getpid()
- 头文件:
#include <sys/types.h>
#include <unistd.h>
- 函数原型:
pid_t getpid(void);
- 函数说明:获取自己的进程ID号
- 返回值: 成功:本进程的ID号。失败:返回 -1。
(2) 进程替换
exexc函数族,重点学习四种:
- 函数原型:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
- 参数说明:
这四个函数第一个参数都是可执行程序或者脚本的程序名,execl、execv需要带有完整的路径;
第二参数为任意字符,起到占位作用;
第三个或者后面的字符为调用者的参数,参数列表最后以NULL结尾,但是execlp、execvp不需要,只需要带有可执行程序的名称即可,系统自动去环境变量去寻找同名文件,execl、execlp需要NULL结尾。 - 函数后缀说明:
l:list 参数一个个的列出来
v:vector 参数用数组存储起来
p:目标程序,可以省略路径
e:环境变量,不考虑
(3) 进程等待(同步)
1)wait()
- 头文件:
#include<sys/types.h>
#include<sys/wait.h>
- 函数原型:
pid_t wait(int*status);
- 函数说明:父进程调用,等待子进程退出,回收子进程的资源
- 参数说明:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
- 返回值:成功:返回被等待进程pid,失败:返回-1。
- 调用wait()函数的进程会被挂起, 进入阻塞状态,直到wait()捕捉到僵尸子进程并回收该子进程的资源,若没有僵尸子进程,wait()函数则会让进程一直处于阻塞状态。若当前有多个进程, 只需要捕捉到一个僵尸子进程, wait()函数就会返回并是进程恢复执行。
这里涉及到Linux中的两种特殊进程:
孤儿进程:
当父进程在子进程退出之前退出,子进程就变成孤儿进程。此时子进程会被init进程收养,之后由init进程代替原来的父进程完成状态收集工作。
僵尸进程:
僵尸进程几乎放弃了退出前占用的所有内存资源,只在进程列表中保留一个位置,记载进程的退出状态码等信息供父进程收集。若父进程未回收,子进程将一直处于僵尸状态。
2)waitpid()
- 头文件:
#include<sys/types.h>
#include<sys/wait.h>
- 函数原型:
pid_ t waitpid(pid_t pid, int *status, int options);
- 函数说明:父进程调用,等待子进程退出,回收子进程的资源
- 参数说明:
pid:一般是进程的pid, 也可以有其他的取值:
1. pid>0时,pid为指定等待的子进程的pid。只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
2. pid=-1时,等待任何一个子进程退出,没有任何限制,waitpid()函数与wait()函数功能相同。
3. pid=0时,等待同一进程组的所有子进程。
4. pid<-1时,等待指定进程组中的任何子进程,进程组的 ID 等于pid的绝对值。
options:提供控制waitpid()的选项, 该选项是一个常量或由 | 链接的两个常量:
1. WONHANG: 即使进程没有终止,waitpid()也会立即返回,就是不会使父进程阻塞。
2. WUNTRACED: 如果子进程暂停执行,则waitpid()立即返回。 - 返回值:
1. 调用正常,返回子进程的pid。
2. 调用出错,返回-1.
3. 当options的值为WONHANG时,但是没有已经退出的子进程可以收集,返回0。
3、退出
(1)正常退出:
- 在main函数中使用了return返回. (return之后把控制权交给调用函数)
- 调用exit()或者_exit; (exit()之后把控制权交给系统)
(2)异常退出:
- 调用abort函数
- 进程收到某个信号,而该信号是程序中止
不管是哪一种退出方式,最后都会执行内核中的同一代码,这段代码用来关闭进程所用到的已经打开的文件描述符所占用的内存和资源。
(3)exit函数与_exit函数
- _exit函数(系统调用到内核执行):是系统调用,直接返回内核,没有多余的东西。
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
其中,status定义了进程的终止状态,父进程可以通过wait来获取该值。
- exit函数
#include <stdlib.h>
void exit(int status);
其中的参数status和_exit中的参数是一样的,都表明了进程的终止状态。
exit函数所做的事情:
第一件事刷新输出缓冲区,接着在做一大堆事情,最后调用_exit函数,准确来说是下面三个事情:
1、执行用户通过atexit或者_exit定义的清理函数。
2、关闭所有打开的流,并且将所有的缓存数据写入。
3、调用_exit函数进入内核。
- exit函数与_exit函数的区别
(三)进程间通信
详见我的另一篇博客 :进程间通信