一、基本概念
1、进程与程序
程序是存储在磁盘上的文件,它是包含要执行的机器指令和数据的静态实体。
进程是一个正在运行的程序,一个程序可能包含多个进程(多任务、多进程),进程在操作系统中是一个执行任务的单位。
2、进程的分类
交互进程:需要用户输入数据,也会显示一些结果给用户看。
批处理进程:用来执行脚本的进程,例如Makefile。
守护进程:它是一种一直活跃的进程,一般是后台的,由操作系统启动时通过开过开机脚本加载或由超级用户加载。
3、查看进程
ps:显示当用户当前终端所控制的进程。
-a:显示所有用户的进程
-x:包括无终端控制的进程
-u:显示详细信息
-w:以更宽的方式显示
USER:属主 PID:进程号 %CPU:cpu占用率 %MEM:内存使用率 VSZ:虚拟内存的大小
RSS:物理内存的使用量 TTY:终端设备号,如果不是终端控制进程用'?'表示 STAT:终端的状态
START:开始时间 TIME:运行时间 COMMAND:开启此进程的命令
4、进程的状态
O:就绪态,一切准备工作都已经做好,等待被调用。
R:运行态,Linux下没有就绪态,O也就是R。
S:可唤醒的睡眠态,系统调用、获取到资源、收到信息都可以被唤醒。
D:不可唤醒的睡眠态,必须等到的事件发生。
T:暂停态,收到了SIGSTOP信号,当收到SIGCONT信号则继续运行。
X:死亡态。
Z:僵尸态。
<:高优先级。
N:低优先级。
L:多线程进程。
s:有子进程的进程。
+:后台进程组。
5、父子进程
如果进程B是由进程A开启的,那么我们把进程A叫进程B的父进程,进程B叫作进程A的子进程。
当子进程结束后会向父进程发送,SIGCHLD,父进程收到信号后再回收子进程。
当先父进程先结束,子进程就会变成孤儿进程,会被孤儿院(init)收养。
如果子进程已经结束,但父进程没有及时回收,子进程就变成了僵尸进程。
6、进程标识符,
俗称进程号,它是一个无符号整数,使用getpid函数可以获取到。
这种编号是循环使用,当进程结束后,它就跟这个编号没有关系,这个编号也会被再次使用,即延时重用。
二、getxxid
pid_t getpid(void); 功能:获取进程id
pid_t getppid(void); 功能:获取父进程id
uid_t getuid(void); 功能:获取实际调用者的用户id
uid_t geteuid(void); 功能:获取程序的拥有者用户的id
gid_t getgid(void); 功能:获取实际使用者的组id
gid_t getegid(void); 功能:获取程序的拥有者用户组id
三、fork
pid_t fork(void);
功能:创建子进程
1、失败返回-1,如果成功会返回两次。
2、父进程会返回子进程的id,子进程返回0
3、根据返回值的不同,分别为子进程和父进程设计不同的分支。
4、通过fork创建出的子进程,就是父进程的副本,它会把父进程的堆、栈、全局段、静态数据段、IO流的缓冲区都拷贝一份,父子进程共享代码段。
5、fork函数调用成功后,父子进程就开始各自执行了,它们的先后顺序是不确定的,但可以通过某些实现来保证。
6、当总进程数超过系统的限制时,就无法再创建进程的了,此时fork函数会返回-1,执行失败。
7、孤儿进程:子进程还有运行,但父进程已经结束,此时子进程会被init(1)收养,子进程变成孤儿进程。
8、僵尸进程:子进程已经死亡,但父进程没有及时回收子进程,此时子进程就会变成僵尸进程。
9、父进程打开的文件和子进程是共享的。
注意:fork之前的代码,只有父进程在执行,fork之后的代码父子进程都有机会执行,根据fork返回值来控制进入不同的分支。
#include <stdio.h> #include <unistd.h> int main() { int pid = fork(); if(0 == pid) { printf("进程id:%d 父进程id:%d ", getpid(),getppid()); } sleep(1); printf("进程id:%d 子进程id:%d ", getpid(),pid); }
执行结果为:
四、vfork,execl
pid_t vfork(void);
功能:创建子进程
1、vfork不能单独创建子进程,需要与excl函数簇,配合才完成子进程的创建。
2、它不会复制父进程的栈、堆、数据、全局等段,也不会共享代码段,而是通过excl函数调用一个程序直接启动,从面提高创建进程的效率。
3、使用vfork创建的子进程保证,先执行子进程,后执行父进程。
execl函数
int execl(const char *path, const char *arg, ...);
path:可执行文件的路径
arg:给可执行文件的参数,类似于命令行参数,必须以NULL结尾,第一个必须是可以执行文件名。
execl("","a.out",NULL);
五、进程的正常退出
1、从main退出,在main中执行return 0;
返回值的低8位会被父进程获取到。
使用_exit/_Exit退出前会关闭所有打开的文件流,如果有子进程则会托附给init,然后向父进程发送SIGCHLD信号。
此函数不会返回。
2、调用标准C的exit(stat)函数
exit在底层实现上调用了_exit/_Exit函数,所以_exit/_Exit的特点它都具备。
exit结束前会调用通过atexit/on_exit注册的函数。
int atexit(void (*function)(void));
int on_exit(void (*function)(int , void *), void *arg);
3、最后一个线程正常结束
#include <stdio.h> #include <stdlib.h> #include <unistd.h> void exit_func(int stats,void * str) { printf("我是以%d的状态结束的,我最后遗言是:%s ",stats,str); } int main() { on_exit(exit_func,"我走了"); for(int i=0; i<5; i++) { sleep(1); printf("%d ",i++); } exit(110); return 1; }
程序执行结果为:
六、进程异常中止
1、进程调用了abort函数(段错误、浮点异常)
2、进程接收到某些信号
crtl+c crtl+ ctrl+z
3、最后一个线程收到取消操作,而且线程作出响应。
七、子进程的回收
pid_t wait(int *status);
功能:等待子进程结束,并回收
pid_t waitpid(pid_t pid, int *status,int options);
功能:等待指定的子进程结束,并回收
1、父进程调用wait
如果所有的子进程都在运行,则父进程阻塞(wait),只要有一个子进程结束了,会立即返回子进程的id和结束状态
当所有子进程都结束运行时,wait会返回-1。当不需要子进程的结束状态时,status的值可为NULL,可利用 while(-1 != wait(NULL)) 回收所有子进程。
2、waitpid函数可以指定等待哪个子进程结束
pid:指定的pid
== -1 功能与wait类似,pid就无意义了。
> 0 等待进程号是pid的进程结束。
== 0 等待与父进程同组的字进程结束。
< -1 等待组id是pid的绝对值的进程组中任意进程结束。
status:用于接收子进程的结束状态,如果不需要状态码可以设置为NULL;
options:
0 以阻塞状态等待子进程结束
WNOHANG 如果没有子进程退出会立即返回。
WUNTRACED 等待的进程处于停止状态,并且之前没有报告过,则立即返回。
3、如果不调用wait/waitpid函数,子进程结束后就处于僵尸状态,当父进程也结束时,父进程的父进程会把他们统一回收。