一、进程的基本概念
1、进程与程序
程序是存储在磁盘上的文件,它是包含要执行的机器指令和数据的静态实体。
进程是一个正在运行的程序,一个程序可能包含多个进程(多任务、多进程),进程在操作系统中是一个执行任务的单位。
2、进程的分类
交互进程:需要用户输入数据,也会显示一些结果给用户看。
批处理进程:用来执行脚本的进程,例如Makefil。
守护进程:它是一种一直活跃的进程,一般是后台的,由操作系统启动时通过开过开机脚本加载或由超级用户加载。
二、在linux下的一些关于进程的命令
ps:显示当用户当前终端所控制的进程。
-a:显示所有用户的进程
-x:包括无终端控制的进程
-u:显示详细信息
-w:以更宽的方式显示
ps aux ps aux|grep pid(进程id)
三、进程信息表
USER:属主
PID:进程号
%CPU:cpu占用率
%MEM:内存使用率
VSZ:虚拟内存的大小
RSS:物理内存的使用量
TTY:终端设备号,如果不是终端控制进程用'?'表示。
STAT:终端的状态
START:开始时间
TIME:运行时间
COMMAND:开启此进程的命令
四、进程的状态
O:就绪态,一切准备工作都已经做好,等待被调用。
R:运行态,Linux下没有就绪态,O也就是R。
S:可唤醒的睡眠态,系统调用、获取到资源、收到信息都可以被唤醒。
D:不可唤醒的睡眠态,必须等到的事件发生。
T:暂停态,收到了SIGSTOP信号,当收到SIGCONT信号则继续运行。
X:死亡态。
Z:僵尸态。
<:高优先级。
N:低优先级。
L:多线程进程。
s:有子进程的进程。
+:后台进程组。
五、父子进程
如果进程B是由进程A开启的,那么我们把进程A叫进程B的父进程,进程B叫作进程A的子进程。
当子进程结束后会向父进程发送,SIGCHLD,父进程收到信号后再支回收子进程。
当先父进程先结束,子进程就会变成孤儿进程,会被孤儿院(init)收养。
如果子进程已经结束,但父进程没有及时回收,子进程就变成了僵尸进程。
六、进程的创建
1、fork函数
pid_t fork(void);
功能:创建子进程
- #include <unistd.h>
- #include <stdio.h>
- int main ()
- {
- pid_t fpid; //fpid表示fork函数返回的值
- int count=0;
- fpid=fork();
- if (fpid < 0)
- printf("error in fork!");
- else if (fpid == 0) {
- printf("i am the child process, my process id is %d/n",getpid());
- printf("我是爹的儿子/n");//对某些人来说中文看着更直白。
- count++;
- }
- else {
- printf("i am the parent process, my process id is %d/n",getpid());
- printf("我是孩子他爹/n");
- count++;
- }
- printf("统计结果是: %d/n",count);
- return 0;
- }
运行结果是:
i am the child process, my process id is 5574
我是爹的儿子
统计结果是: 1
i am the parent process, my process id is 5573
我是孩子他爹
统计结果是: 1
在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)……
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
************
关于fork()的几点说明
1、失败返回-1,如果成功会返回两次。
2、父进程会返回子进程的id,子进程返回0
3、根据返回值的不同,分别为子进程和父进程设计不同的分支。
4、通过fork创建出的子进程,就是父进程的副本,它会把父进程的堆、栈、全局段、静态数据段、IO流的缓冲区都拷贝一份,父子进程共享代码段。
5、fork函数调用成功后,父子进程就开始各自执行了,它们的先后顺序是不确定的,但可以通过某些实现来保证。
2、vfork函数
pid_t vfork(void);
功能:创建子进程
1、vfork不能单独创建子进程,需要与excl函数簇,配合才完成子进程的创建。
2、它不会复制父进程的栈、堆、数据、全局等段,也不会共享代码段,而是通过excl函数调用一个程序直接启动,从面提高创建进程的效率。
3、使用vfork创建的子进程保证,先执行子进程,后执行父进程。
exec函数簇
exec函数的功能:加载一个可执行文件,要和vfork函数配合才有意义。
int execl(const char *path, const char
*arg, ...);
path:可执行文件的路径
arg:给可执行文件的参数,类似于命令行参数,必须以NULL结尾,第一个必须是可以执行文件名。
execl("","a.out",NULL);
int execlp(const char *file, const char
*arg, ...);
file:可执行文件的文件名,会从PATH环境变量指定的位置去找可执行文件。
int execle(const char *path, const char
*arg,..., char * const envp[]);
int execv(const char *path, char *const
argv[]);
int execvp(const char *file, char *const
argv[]);
例子程序:
#include <stdio.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
printf("我是进程%d
",getpid());
for(int i=0; i<argc; i++)
{
printf("%s
",argv[i]);
}
while(1)
{
sleep(1);
printf("hello world!
");
}
}
#include <stdio.h>
#include <unistd.h>
int main()
{
int pid = vfork();
if(0 == pid)
{
printf("我是进程%d
",getpid());
sleep(3);
execl("/home/zhizhen/hello","hello","haha","hehe","你好",NULL);
}
printf("我是%d的父进程%d
",pid,getpid());
while(1)
{
printf("大家好,才是真的好!
");
sleep(1);
}
}
说明:先有一个hello的程序创建出可执行文件hello 再在另一个程序中使用vfork
注意:
1、通过exec创建的子进程会替换掉父进程给的代码段,不拷贝父进程的堆、栈、全局、静态数据段,会用新的可执行文件替换掉他们。
2、exec只是加载一个可执行文件,并不创建进程,不会产生新的进程号。
3、只有exec函数执行结束(无论成功还是失败),父进程才能继续执行。
七、进程的正常退出
1、从main退出,在main中执行return 0;
返回值的低8位会被父进程获取到。
2、系统的_exit(stat)/标准的_Exit(stat),这两个函数几乎没有什么区别。
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
a、返回值的低8位会被父进程获取到。
b、使用_exit/_Exit退出前会关闭所有打开的文件流,如果有子进程则会托附给init,然后向父进程发送SIGCHLD信号。
c、此函数不会返回。
3、调用标准C的exit(stat)函数
a、exit在底层实现上调用了_exit/_Exit函数,所以_exit/_Exit的特点它都具备。
b、exit结束前会调用通过atexit/on_exit注册的函数。
int atexit(void (*function)(void));
int on_exit(void (*function)(int , void *), void *arg);
4、最后一个线程正常结束
八、进程异常中止
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、当一个进程结束时,内核会向它的父进程发送一个信号
父进程收到这个信号后可以指定一个函数来处理,也可以选择忽略,默认情况下是忽略的。
2、父进程调用wait调用才是有意义
如果所有的子进程都在运行,则父进程阻塞(wait)
只要有一个子进程结束了,会立即返回子进程的id和结束状态。
当所有子进程都结束运行时,wait会返回-1。
如果在调用wait之前子进程就已经结束(僵尸子进程),执行wait函数时会立即返回并回收僵尸进程。
3、waitpid函数可以指定等于哪个子进程
pid:指定的pid
== -1 功能与wait类似,pid就无意义了。
> 0 等待进程号是pid的进程结束。
== 0 等待组id等于pid的进程组中任意进程结束。
< -1 等待组id是pid的绝对值的进程组中任意进程结束。
status:用于接收子进程的结束状态,如果不需要状态码可以设置为NULL;
options:
0 以阻塞状态等待子进程结束
WNOHANG 如果没有子进程退出会立即返回。
WUNTRACED 等待的进程处于停止状态,并且之前没有报告过,则立即返回。
4、如果不调用wait/waitpid函数,子进程结束后就处于僵尸状态,当父进程也结束时,父进程的父进程会把他们统一回收。
最后关于僵尸进程和子进程的一些理解:
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。