Linux进程管理
一、进程的基本概念
1、进程与程序
程序是存储在磁盘上的可执行文件,程序被加载到内存中开始运行时叫做进程
一个程序可以被多次加载生成多个进程,进程就是处于活动状态的计算机程序
2、进程的分类
进程一般分为三大类:交互进程、批处理进程、守护进程
3、查看进程
简单模式:ps 显示当前用户有终端控制进程简单信息
列表模式:ps -auxw 显示所有进程的详细信息
a 所有用户的有终端控制的进程
x 无终端控制的进程
u 显示所有进程的详细信息
w 以更大的列宽显示
USER 进程的属主用户名
PID 进程号
%CPU CPU的使用率
%MEM 内存的使用率
VSZ 虚拟内存使用的字节数
RSS 物理内存使用的字节数
TTY 终端设备号 '?'表示无终端控制
STAT 进程的状态
O 就绪态 等待被调用
R 运行态 Linux系统中没有O,就绪也用R来表示
S 可被唤醒的睡眠态 例如系统中断、获取资源、收到信号等都可以唤醒进入运行态
D 不可被唤醒的睡眠态 只能被系统唤醒
T 暂停态 收到SIGTSTP信号进入暂停态,收到SIGCONT信号转回运行态
X 死亡态
Z 僵尸态(将死态)
N 低优先级
< 高优先级
l 多线程进程
s 进程的领导者
START 进程的启动时间
TIME 进程的运行时间
COMMAND 启动进程的命令
4、父进程、子进程、孤儿进程、僵尸进程
一个进程可以被另一个进程创建,创建者叫做父进程,
被创建者叫子进程,子进程被父进程创建后会在操作系统的调度下同时运行
当子进程先于父进程结束,死前子进程会向父进程发送信号SIGCHLD,此时父进程应该去回收子进程的相关资源
孤儿进程:父进程先于子进程结束,子进程就变成了孤儿进程,孤儿进程会被孤儿院(init守护进程)领养
init就是孤儿进程的父进程
僵尸进程:该进程已死亡,但是它的父进程没有立即回收它的相关资源,该进程就进入僵尸态
5、进程标识符
每个进程都有一个非负整数表示唯一标识,即进程ID/PID
进程ID在任意时刻都是唯一的,但是可以重新使用,进程一旦结束,它的进程ID就会被系统回收,过一段时间后再重新分配给其他新创建
的进程使用(延时重用)
pid_t getpid(void);
功能:返回调用者的进程ID
pid_t getppid(void);
功能:返回父进程的ID
二、创建进程
pid_t fork(void);
功能:创建子进程
返回值:一次调用两次返回,子进程返回0,父进程返回子进程的ID,当进程数量超过系统的限制时会创建失败,返回-1
1、通过fork创建的子进程会拷贝父进程(数据段、bss段、堆、栈、I/O缓冲区),与父进程共享代码段、子进程会继承父进程的
信号处理方式
2、fork函数调用后父子进程各自独立运行,谁先返回不确定,但是可以通过睡眠确定让哪个进程先执行
3、通过fork创建的子进程可以共享父进程的文件描述符
4、可以根据返回值的不同让父子进程进入不同的分支,执行不同的代码
练习1:为进程创建4个子进程,再为这4个子进程,分别创2个子进程
解题思路:子程序创建后,执行完任务后就休眠,这样不会被其他的fork影响
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main(int argc,const char* argv[])
5 {
6 printf("我是进程%u\n",getpid());
7 for(int i = 0;i<4;i++)
8 {
9 if(0 == fork())
10 {
11 printf("我是进程%u,我的父进程是%u\n",getpid(),getppid());
12 for(int j = 0;j<2;j++)
13 {
14 if(0 == fork())
15 {
16 printf("我是进程%u,我的父进程是%u\n",getpid(),getppid());
17 pause;
18 }
19 }
20 pause();
21 }
22 }
23 pause();
24 }
pid_t vfork(void);
功能:以加载可执行文件的方式来创建子进程
返回值:子进程返回0,父进程返回子进程的ID
注意:vfork创建的子进程一定先返回,此时子进程并没有创建成功,需要加载一个可执行文件替换当前子进程所有资源,当替换完成后子进程
才算创建成功,此刻父进程才返回
使用exec 系列函数让子进程加载可执行文件
extern char **environ;
int execl(const char *path, const char *arg, ...
/* (char *) NULL */);
path:可执行文件的路径
arg:命令行参数,个数不定,由实际的可执行文件所需命令行参数决定
一般第一个是可执行文件的名字,至少有一个,一定要以NULL结尾
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
file:可执行文件名字
arg:命令行参数,同上
注意:会去系统默认路径PATH 指定的路径下加载file
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
path:可执行文件的路径
arg:命令行参数,同上
envp:环境变量表,父进程可以在加载子进程时把环境变量表传递给子进程
int execv(const char *path, char *const argv[]);
path:可执行文件的路径
argv:命令行参数数组,最后以NULL结尾
int execvp(const char *file, char *const argv[]);
file:可执行文件名字
argv:命令行参数数组,同上
注意:也是根据PATH的路径加载file
int execvpe(const char *file, char *const argv[],char *const envp[]);
file:可执行文件名字
argv:命令行参数数组,同上
envp:环境变量表
注意:也是根据PATH的路径加载file
exec系列函数正常情况下是不会返回的,当子进程加载失败时才会返回-1
虽然以vfork、exec系列函数创建加载的子进程不会继承父进程的信号处理函数,但是能继承父进程的信号屏蔽集
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,const char* argv[],char** environ)
{
pid_t pid = vfork();
if(0 == pid)
{
//execlp("hello","hello","-l",NULL);
char* argv[] = {"hello","-a",NULL};
execvpe("hello",argv,environ);
printf("我还在!\n");
}
else
{
printf("我是进程%u,我的子进程是%u\n",getpid(),pid);
}
}
练习2:实现出孤儿进程和僵尸进程 ps查看
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,const char* argv[])
{
/*
// 孤儿进程
if(fork())
{
printf("我是父进程%u\n",getpid());
sleep(3);
printf("我要死啦!\n");
}
else
{
for(;;)
{
printf("我是子进程%u,我的父进程是%u\n",
getpid(),getppid());
sleep(1);
}
}
*/
// 僵尸进程
if(fork())
{
for(;;)
{
printf("我是父进程%u\n",getpid());
sleep(1);
}
}
else
{
printf("我是子进程%u,我的父进程是%u\n",
getpid(),getppid());
sleep(2);
printf("我要完了\n");
}
}
三、进程的正常退出
1、在main函数中执行 return n;该返回值n可以被父进程获取,几乎与exit等价
2、进程调用了exit函数,该函数是C标准库中的函数
void exit(int status);
功能:在任何时间、地点调用该函数都可以立即结束进程
status:结束状态码(EXIT_SUCCESS/EXIT_FAILURE),与main函数中return的返回值效果是一样的
返回值:该函数不会返回
进程退出前要完成:
1、先调用事先通过atexit\on_exit函数注册的函数,如果都注册了执行顺序与注册顺序相反
int atexit(void (*function)(void));
功能:向内核注册一个进程结束前必须调用的函数
int on_exit(void (*function)(int , void *), void *arg);
功能:向内核注册一个进程结束前必须调用的函数
arg:会在调用function时传给它
2、冲刷并关闭所有打开状态的标准IO流
3、底层继续调用_Exit/_exit函数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void atexit_fp(void)
{
printf("我要死啦!\n");
}
void on_exit_fp(int num,void* ptr)
{
if(EXIT_SUCCESS == num)
{
printf("寿终正寝!\n");
}
else
{
printf("有人%d想搞我,%s\n",num,(char*)ptr);
}
}
int main(int argc,const char* argv[])
{
on_exit(on_exit_fp,"呵呵");
atexit(atexit_fp);
printf("我还活着!\n");
sleep(3);
exit(-10);
// return 0;
printf("----\n");
}
3、调用_Exit/_exit函数
void _exit(int status);
功能:结束进程,由系统提供的
void _Exit(int status);
功能:结束进程,由标准库提供的
1、它们的参数会被父进程获取到
2、进程结束前会关闭所有处于打开状态的文件描述符
3、向父进程发生信号SIGCHLD
4、该函数也不会返回
4、进程的最后一个线程执行了return返回语句
5、进程的最后一个线程执行了pthread_exit函数
四、进程的异常终止
1、进程调用了about函数,产生SIGABRT(6)信号
2、进程接收到某些信号,可以是其他进程发送的,也可能是自己的错误导致的
3、进程的最后一个线程接收到"取消"请求操作,并响应
这三种方式结束进程,它的父进程都无法获取结束状态码,因此叫做异常终止
注意:无论进程是如何结束的,它们最后都会执行同一段代码,会关闭所有打开的文件,并释放所有的内存
五、子进程的回收
对于子进程的结束而言,都希望父进程能够知道并作出一定的反应,通过wait、waitpid函数可以知道子进程是如何结束的以及它的结束状态码
pid_t wait(int *status);
功能:等待子进程结束,并获取结束状态码
status:输出型参数,接收结束状态码
返回值:结束的子进程的ID
1、如果所有子进程都还在运行,则阻塞
2、如果有一个子进程结束,立即返回该进程的结束状态码和ID
3、如果没有子进程返回-1
WIFEXITED(status)
判断进程是否是正常结束,如果是返回真
WEXITSTATUS(status)
如果进程是正常结束的,可以获取到正确的结束状态码
WIFSIGNALED(status)
判断进程是否是异常,如果是返回真
WTERMSIG(status)
如果进程是异常结束的,可以获取到杀死进程的信号
WIFSTOPPED(status)
WSTOPSIG(status)
WIFCONTINUED(status)
pid_t waitpid(pid_t pid, int *status, int options);
功能:等待回收指定的某个或某些进程结束
pid:
>0 等待该进程结束
0 等待同组的任意进程结束
-1 等待任意进程结束、功能与wait等价
<-1 等待abs(pid)进程组中的任意进程结束
status:输出型参数,接收结束状态码
options:
WNOHANG 非阻塞模式,如果当前没有子进程结束,则立即返回0
WUNTRACED 如果有子进程处于暂停态,返回该进程的状态
WCONTINUED 如果有子进程从暂停态转为继续运行,返回该子进程的状态
WIFSTOPPED(status)
判断子进程是否转为暂停态,是返回真
WSTOPSIG(status)
获取导致子进程进入暂停态的信号
WIFCONTINUED(status)
判断子进程是否由暂停转为继续,是返回真
int system(const char *command);
功能:通过创建子进程去执行一个可执行文件
返回值:子进程结束后才返回
注意:该函数底层调用了fork、vfork、exec函数、waitpid函数
练习1:综合进程管理知识点,实现一个system
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int _system(const char* command)
{
pid_t pid = vfork();
if(0 == pid)
{
return execlp(command,command,NULL);
}
else
{
int status = -1;
waitpid(pid,&status,0);
return status;
}
}
int main(int argc,const char* argv[])
{
_system("ls");
printf("end!\n");
}