一、进程介绍
进程与程序:
-
程序是存储在磁盘上的可执行文件,里面包含可执行的机器指令和数据的静态实体;进程是处于活跃状态的计算机程序,也就是正在运行中的程序
-
一个运行中的程序,可能由多个进程组成,但至少要有一个进程,称为主进程,同时可以通过系统调用创建出若干个子进程同时进行任务
-
一个程序也可以同时运行出若干个进程
进程的分类:
根据进程的功能不同一般分为三类:交互进程、批处理进程、守护进程
-
交互进程:由一个shell终端启动的进程,在执行过程中,需要与用户进行交互操作,可以运行在前台,也可以运行在后台
-
批处理进程:该进程是一个进程指令集合,负责按顺序去启动其他进程
-
守护进程:一般都处于活跃状态,运行在后台,由系统在开机时通过脚本自动创建并运行。
进程查看:
简单形式:
ps: 以简略的形式显示出当前用户有控制终端控制的进程信息
复杂形式:
ps auxw 以更宽大的列表形式详细地列出所有用户的进程信息
a - 所有用户的有终端控制的进程
x - 包括无终端控制的进程
u - 以更详细的内容显示
w - 以更大的列宽显示
e - 显示所有进程
f - 显示出其他信息字段
进程的信息列表:
-
USER : 进程属主
-
PID : 进程ID
-
%CPU : CPU使用率
-
%MEM :内存使用率
-
VSZ : 占用虚拟内存大小(Kb)
-
RSS : 占用物理内存大小(Kb)
-
TTY :控制终端设备号 ? 表示无终端控制 ,例如后台进程
-
STAT :进程状态 ,可有以下值:
-
O - 就绪态 ,表示等待被调度
-
R - 运行态,Linux下没有O状态,就绪态也用R表示
-
S - 可被唤醒睡眠态。当系统中断、获得资源、收到信号等都可以被唤醒转入回运行态
-
D - 不可被唤醒睡眠态。只能被wake_up系统调用唤醒
-
T - 暂停态。收到停止类信号转入暂停态,当收到SIGCONT(18)转入运行态
-
Z - 僵尸态。已经停止运行,但是父进程尚未回收相关资源
-
X - 死亡态。不可见
-
N - 低优先级
-
< - 高优先级
-
s - 进程组的领导
-
l - 多线程化的进程
-
+ 在前台的进程组中的
-
L - 有被锁入内存的分页
-
-
START : 进程启动的时间点
-
TIME : 进程运行的耗时时间
-
COMMAND :启动进程的指令
# 查看指定进程 ps aux | grep bash #过滤出包含bash关键字的进程信息 # 分页查看进程 ps aux | more # 查看指定用户进程 ps -u 用户名 uw
父进程与子进程:
-
一个进程可以创建出另一个进程,创建者称为被创建者的父进程,被创建者称为创建者的子进程
-
父进程创建出子进程后,子进程在操作系统的调度下与父进程同时运行
孤儿进程与僵尸进程:
-
子进程先于父进程结束,子进程一定会向父进程发送SIGCHLD(17)信号,父进程负责回收子进程的相关资源
-
如果父进程先于子进程结束,此时子进程称为孤儿进程,同时会被孤儿院进程收养,就成为了孤儿院进程的子进程
-
早期孤儿院进程init pid是1
-
现在孤儿院进程不是1了,在图形化界面中是/sbin/upstart --user
-
-
子进程先于父进程结束,但是父进程没有去回收子进程相关资源,该子进程就成为僵尸进程
进程标识符:
-
每个进程都有一个以非负整数表示的唯一标识,称为进程ID,简称PID
-
进程ID在任意时刻内是唯一的,但是可以重用,当一个进程结束后,它的进程ID就会被分配个后面创建的其他进程使用
-
延时重用:当进程结束后,它的ID不会立即被系统重新分配,会隔一段时间后再重新分配
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); 功能:获取当前进程的ID pid_t getppid(void); 功能:获取当前进程的父进程ID
二、fork创建子进程
#include <unistd.h> pid_t fork(void); 功能:创建一个子进程 返回值:创建失败返回-1 创建成功会返回两次 父进程:返回子进程的pid 子进程:返回0 注意:总进程数或者实际拥有pid的进程数量超过了系统的限制,该函数失败
注意:子进程创建出来后,父子进程会同时各自运行代码,因此可以通过分支判断返回值,来让父子进程执行不同的程序代码
#include <stdio.h> #include <unistd.h> int main(int argc,const char* argv[]) { printf("我是进程%u\n",getpid()); pid_t pid = fork(); if(-1 == pid) { perror("fork"); return -1; } if(0 == pid) { printf("我是子进程%u,我的父进程是%u\n",getpid(),getppid()); pause(); } else { printf("我是父进程%u,我的子进程是%u\n",getpid(),pid); pause(); } }
父子进程谁先运行:
-
通过fork系统调用创建出来的子进程与它父进程会各自往下运行,但是其先后顺序不确定,可以通过睡眠等系统调用确定让哪个进程先执行
子进程是父进程的副本:
-
由fork创建的子进程会获得拷贝出父进程的data段、bss段、heap段、stack段、I/O流缓冲区。
#include <stdlib.h> int main(int argc,const char* argv[]) { int num = 0; int* p = malloc(4); if(fork()) { sleep(1); // 父进程 num = 1000; *p = 1000; } else { // 子进程 num = 2000; *p = 2000; } // 各自的num都没有被其他进程改变,证明父子进程的num不是同一个,是子进程拷贝了父进程的数据 // 虽然父子进程中的num 和p的地址是相同的,但是每个进程都拿到4g的虚拟内存,但是映射的物理内存是不一样的,所以虚拟地址相同没有参考价值 printf("pid=%u : num=%d *p=%d &num=%p p=%p\n", getpid(),num,*p,&num,p); sleep(2); printf("pid=%u : num=%d *p=%d &num=%p p=%p\n", getpid(),num,*p,&num,p); }
#include <stdio.h> #include <unistd.h> int main(int argc,const char* argv[]) { // *会残留在输出缓冲区,被拷贝给子进程 // 子进程创建后会继续代码,有可能也会创建子进程 printf("*"); for(int i=0; i<3; i++) { fork(); } }
子进程会共享父进程的代码段、文件描述符fd:
-
通过fork创建的子进程会共享父进程的代码段,fork之前的代码只有父进程执行,fork之后的代码父子进程都有机会执行,主要受到逻辑的控制进入不同的分支
-
不同的程序之间,文件描述符是不能共享的
-
但是由fork创建的父子进程之间,是把父进程内核中的文件描述符的表格拷贝给了子进程,此时两者共享父进程的已打开的文件描述符
fork子进程会继承父进程的信号处理方式:
-
通过fork创建子进程会继承父进程的信号处理方式,是因为子进程共享了父进程的代码段
#include <signal.h> void sigint(int num) { printf("我是进程%u,获得了%d信号\n",getpid(),num); } int main(int argc,const char* argv[]) { signal(SIGINT,sigint); if(fork()) { printf("我是父进程%u\n",getpid()); for(;;); } else { printf("我是子进程%u\n",getpid()); for(;;); } }
练习1:实现出孤儿进程与僵尸进程,根据ppid和ps命令查看
#include <stdio.h> #include <unistd.h> int main(int argc,const char* argv[]) { pid_t pid = fork(); if(pid) { for(;;) { printf("我是父进程%u,我的子进程是%u\n",getpid(),pid); sleep(1); } } else { printf("我是子进程%u,\n",getpid()); sleep(3); printf("我私了!\n"); } /* 孤儿进程 if(fork()) { printf("我是父进程%u\n"); sleep(3); printf("我是父进程我要死了\n"); } else { for(;;) { printf("我是子进程%u,我的父进程是%u\n", getpid(),getppid()); sleep(1); } } */ }
练习2:给主进程创建出4个子进程,再给每个子进程创建2个子进程
#include <stdio.h> #include <unistd.h> int main(int argc,const char* argv[]) { printf("我是主进程%u\n",getpid()); for(int i=0; i<4; i++) { if(0 == fork()) { printf("我是进程%u,我的父进程%u\n",getpid(),getppid()); for(int i=0; i<2; i++) { if(0 == fork()) { printf("我是孙子进程%u,我的父进程%u\n", getpid(),getppid()); pause(); } } pause(); } } pause(); }
三、vfork和exec系列函数创建进程
#include <sys/types.h> #include <unistd.h> pid_t vfork(void); 功能:创建一个子进程,返回值特点与fork没有区别
vfork的特点:
-
当调用vfork系统调用时,父进程会进入阻塞状态,子进程一定先返回执行
-
子进程返回时,先临时使用父进程的相关资源,然后等待exec系列函数执行加载一个可执行文件,从而让子进程去启动另一个程序,把那个程序的资源替换自己原来的资源。
-
当子进程调用完exec系列函数,替换完原来的所有资源后,子进程才算真正创建完毕,此时父进程才会接触阻塞状态,返回子进程的pid
-
如果子进程不调用exec系列函数后果:
-
情况1:子进程一直没有创建成功,导致父进程一直处于阻塞状态,无法返回
-
情况2:子进程直接结束并释放相关资源,此时子进程使用的还是父进程的资源,父进程会返回,但会产生段错误,因为它的相关资源已经被子进程错误释放掉了
-
-
vfork不能单独创建出子进程,必须与exec系列函数中某个函数配合使用
fork和vfork的区别:
-
vfork调用后,子进程先返回,而fork调用,谁先返回不确定
-
vfork不会复制、共享父进程的相关资源,而是去加载其他程序,替换原来的临时资源
-
以exec系列函数创建的子进程不会继承父进程的信号处理方式,但是可以继承父进程的信号屏蔽
exec系列函数:
功能:都是为了与vfork配额创建子进程的函数 int execl(const char *path, const char *arg, ... /* (char *) NULL */); path:要加载的程序的路径 arg:命令行参数,最起码第一个是执行可执行文件的命令,最后一个以NULL结尾 int execlp(const char *file, const char *arg, ... /* (char *) NULL */); file:只需要被加载程序的文件名,系统会根据环境变量PATH中的路径去查找该文件 int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */); envp:环境变量表,相当于父进程把自己的环境变量表拷贝给子进程 int execv(const char *path, char *const argv[]); argv:把命令行参数以字符串指针数组方式提供,注意:一定要以NULL结尾 int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);
#include <stdio.h> #include <unistd.h> int main(int argc,const char* argv[]) { pid_t pid = vfork(); if(0 == pid) { printf("我是子进程%u\n",getpid()); // 加载其他程序 //execl("hello","hello","xixi","10",NULL); char* a[] ={"hello","hehe","xx",NULL}; execv("hello",a); printf("-------------\n"); } else { printf("我是父进程%u\n",getpid()); } printf("************\n"); }
四、进程的正常结束
main函数中执行了return 结束进程
int main(...) { ... return x; // 该返回值可以被父进程接收 } // 等价于 int main(...) { ... exit(x) // 该返回值可以被父进程接收 }
调用标准C的exit函数 结束进程
#include <stdlib.h> void exit(int status); status:进程的结束状态码
-
该函数一旦调用就不会返回,其父进程通过wait\waitpid函数可以获取到status的低8位数据
-
进程正常退出前会先调用事先通过atexit\on_exit函数注册过的函数,然后冲刷并关闭所有处于打开状态下的标准I/O流
-
可以用 EXIT_SUCCESS和EXIT_FAILURE常量作为exit或return的结束状态码,表示进程是正常结束,还是出问题结束的
-
该函数底层调用了 _exit \ _Exit函数
#include <stdlib.h> int atexit(void (*function)(void)); funciton: 函数指针 进程退出前要执行该函数 int on_exit(void (*function)(int , void *), void *arg) funciton: 函数指针 第一个参数:来自return的n或者exit的参数 status 第二个参数,来自on_exit的arg参数 arg:任意类型指针
#include <stdio.h> #include <stdlib.h> #include <unistd.h> void atexit_fp(void) { printf("我就要死了...\n"); } void on_exit_fp(int s,void* p) { printf("我要结束了...status=%d 遗言%s\n",s,(char*)p); } int main(int argc,const char* argv[]) { // 注册遗言函数 // 谁后注册,谁先执行 atexit(atexit_fp); on_exit(on_exit_fp,"是它杀了我"); for(int i=0; i<3; i++) { printf("我是进程%u\n",getpid()); sleep(1); } exit(10); }
调用_exit / _Exit 函数结束进程:
#include <unistd.h> void _exit(int status); # 该函数有一个完全等价的标准C版本 #include <stdlib.h> void _Exit(int status);
-
该函数一旦调用就不会返回,其父进程通过wait\waitpid函数可以获取到status的低8位数据
-
在进程退出前,先关闭所有打开状态下的文件描述符,并把所有的子进程托付给孤儿院进程收养(init\upstart--user),并把当信号SIGCHLD(17)发送给其父进程
其它正常结束进程的方式:
-
进程的最后一个线程执行完毕,进程也结束
-
进程的最后一个线程调用pthread_exit函数,进程也结束
五、进程的异常终止
-
进程收到了某些信号,他杀
-
进程自己调用abort函数,产生了SIGABRT(6)信号,自杀
-
进程的最后一个线程收到了"取消"操作,并且做出响应
-
如果进程是异常结束的,atexit\on_exit它们事先注册的遗言函数不会被调用,也不会冲刷标准IO流
-
但是依然会给父进程发送信号SIGCHLD(17),关闭所有打开状态下的文件描述符
六、子进程的资源回收
-
对于子进程任何方式的结束,都希望父进程能够知道,可以通过wait\waitpid函数可以知道子进程是如何结束的以及它结束状态码
pid_t wait(int *status); 功能:以阻塞状态等待任意一个子进程的结束,并回收它的相关资源,获取到结束状态码 status:获取结束的子进程的结束状态码 是输出型参数 返回值:成功返回结束的子进程的pid,失败的返回-1 1、如果所有子进程都在运行中,则阻塞等待 2、如果有一个子进程结束,则立即返回该子进程的状态码和pid 3、如果当前没有子进程运行,返回-1 对于子进程的结束状态码status可借助宏函数解析判断: WIFEXITED(status) - 子进程是否正常结束 WEXITSTATUS(status) - 当子进程是正常结束时,该宏可以获取到正确的结束状态码的低8位数据 WTERMSIG(status) - 如果进程是异常终止的,该宏可以获取到杀死该子进程的信号编号 pid_t waitpid(pid_t pid, int *status, int options);
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <signal.h> int main(int argc,const char* argv[]) { for(int i=0; i<10; i++) { if(0 == fork()) { printf("我是子进程%u i=%d\n",getpid(),i); sleep(rand()%8+3); if(0 == i%2) { kill(getpid(),3); } return 88+i; } } for(;;) { printf("*\n"); sleep(1); int status = 0; pid_t pid = wait(&status); if(-1 == pid) { printf("子进程都死了\n"); break; } if(WIFEXITED(status)) { printf("是正常结束的 状态码是%d\n",WEXITSTATUS(status)); } else { printf("是异常终止的,被信号%d杀死\n",WTERMSIG(status)); } } }