进程管理
概要
- 进程与程序
- 进程分类
查看
在终端键入以下命令即可查看:
# ps
(1) 简单形式,以简略方式显示当前用户有控制终端的进程信息
# ps aux
(2) 以BSD风格显示
a - 所有用户有控制终端的进程
x - 包括无控制终端的进程
u - 以详尽方式显示
w - 以列大列宽显示
# ps -elf
(3) 以SVR4风格显示
-e / -A - 所有用户的进程
-a - 当前终端的进程
-u uid - 特定用户的进程
-g gid - 特定组的进程
-f - 按完整格式显示
-F - 按更完整格式显示
-l - 按长格式显示
# USER/UID - 进程属主
# PID - 进程ID getpid() 获取当前进行程的ID
# PPID - 父进程ID
# %cpu/C - cpu使用率
# %MEM - 内存使用率
# VSZ - 占用虚拟内存大小(KB)
# RSS - 占用物理内存大小(KB)
# TTY - 终端次设备号 ?表示无终端控制的进程
# STAT/S - 进程状态
O - 就绪,等待被调度
R - 运行,Linux下没有O状态,就绪状态也用R表示
S - 可唤醒的睡眠。系统中断,获得资源,收到信号,都可被唤醒,唤醒之后进入运行状态
D - 不可唤醒的睡眠,只能被wake_up系统调用唤醒
T - 暂停。收到SIGSTOP信号转入暂停状态,收到SIGCONT信号转入运行状态
X - 死亡。不可见
Z - 僵尸进程。已停止运行,但其父进程尚未获取其状态(没有回收资源)
< - 高优先级
N - 低优先级
L - 有被朱丹到内存中的分页。实时进程和定制IO
s - 会话首进程 setuid
l - 多线程化的进程
# START/STIME - 进程开始时间
# TIME - 进程运行时间
# COMMAND/CMD - 进程指令/程序名
# F - 进程标志
1 - 通过fork产生但是没有exec
4 - 拥有超级用户特权
# NI - 进程nice值 -20,19区间取值
# PRI - 进程优先级 静态优先级 = 80+nice 60,99 值越小优先级越高
# ADDR - 内核进程的内存地址,普通进程显示 -
# SZ - 占用虚拟内存页数
# WCHAN - 进程正在等待的内核函数和事件
# PSR - 进程被绑定到哪个处理器cpu
# top linux任务管理器
动态化的ps
按k 再输入 pid 可以关闭某个进程
按q 退出
# ps aux | grep 3025/pid/cmd 查看是否有某个进程
层次
- 父进程
- 子进程
- 孤儿进程
- 僵尸进程
- 开机启动 内核进程(0) 创建子进程
-
init
(1号进程)xinetd
-
- 一个进程可以创建另外一个进程,被创建的进程称为子进程,创建进程的进程称为父进程
- 父进程创建子进程后,子进程在操作系统的调用度与父进程同时运行
- 子进程先于父进程结束 ,子进程向父进程发送
SIGCHLD(17)
信号,父进程回收子进程的相关资源 - 父进程先于子进程结束 ,子进程成为孤儿进程,孤独进程一般会被
init
进程收养,即成为init
进程的子进程 - 子进程先于父进程结束 ,但父进程没有回收子进程的相关资源 ,该子进程即成为僵尸进程
标识
- 每个进程都有一个以非负整数表示的唯一标识,即进程ID ,process id
- 在一个操作系统中,进程ID在任何时刻都是唯一的,唯一标识一个进程
- 进程ID可以重复使用,当一个进程退出以后,其进程ID就可以分配给新的进程使用(取决于操作系统,一般都是延迟重用)
#include <unistd.h>
pid_t getpid(); //获取当前进程ID
pid_t getppid(); //获取父进程的ID 在终端中运行./a.out 进程的父进程永远都为当前终端进程
uid_t getuid(); //实际用户ID 登录的用户
uid_t geteuid(); //有效用户ID 一般而言会等于实际用户ID 如果一个文件拥有S_ISUID设置用户ID位,有效用户ID等于程序的属主用户ID
gid_t getgid(); //实际组ID
gid_t getegid(); //有效组ID
创建子进程函数:fork()
#include <unistd.h>
//创建子进程
pid_t fork(void);
- 创建子进程,失败返回-1
- 如果成功,调用一次,但是返回两个结果
- 分别在父进程中和子进程中都有返回值
- 父进程中返回子进程的ID
- 子进程返回0
- 所以在编程中,可以用返回值作为条件,分别为父子进程编写不同的代码
- 分别在父进程中和子进程中都有返回值
- 写个程序创建10个进程:
#include <stdio.h>
#include <unistd.h>
int main(int argc,const char* argv[])
{
int i = 0;
for(i; i < 9; i++){
pid_t id = fork();
if(id == -1){
perror("fork");
return -1;
}
if(id == 0){
break;
}
}
printf("hello\n");
//getchar();
return 0;
}
总结:直接创建会像分裂一样创建210个进程,需要结束多余的子进程
vfork
#include <unistd.h>
int main()
{
pid_t id = vfork();
if(id == -1){
perror("vfork");
return -1;
}
if(id == 0){
exit(0);//否则产生段错误
}
}
- 功能几乎和fork一致,
- 如果直接为vfork创建的子进程编写代码,不能用return结束子进程,只能用exit(0)结束,但一般来说不会为子进程编写逻辑代码,会调用exec()系列函数族,直接启动另一个进程替换子进程本身,进而提高进程的创建效率
子进程
子进程是父进程的副本
-
子进程创建时获得父进程的数据段和堆栈段(包括IO流缓冲区)的拷贝
-
子进程复制了父进程的内存空间的数据
-
子进程共享父进程的代码段!!!
-
在创建子进程时,并不会在创建子进程时立刻复制父进程的内存空间
- 写时复制(延迟复制) 原因是为了提高创建进程的效率
-
父子进程数据空间虽然拥有相同的标识符,而且标识符拥有相同的内存地址,实际上是两份独立的
-
内存地址 ---------- 虚拟内存
- 不同进程之间的内存地址是毫无意义的
-
如果在创建进程之前,在父进程的有申请动态内存,则子进程中也会拥有和父进程一样的动态内存(包括动态内存中的数据也是一样的),但是,父子进程中的动态内存是两个独立的动态内存,需要分别释放
-
fork函数调用之后,父子进程各自继续运行(子进程的数据空间数据来源于父进程)
- 父子进程运行的先后顺序不确定,某些实现(vfork)可以保证子进程先执行
-
共享文件表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6j8pybLp-1661345891068)(E:%5CTypora’s%20photo%5C%E5%88%9B%E5%BB%BA%E5%AD%90%E8%BF%9B%E7%A8%8B%E4%B9%8B%E5%89%8D%E6%89%93%E5%BC%80%E7%9A%84%E6%96%87%E4%BB%B6.png)]
- 主进程从main函数开始运行,创建的子进程从fork下面开始执行
孤儿进程
- 父进程先于子进程结束,子进程变为孤儿进程,一般被init收养,但现代linux都被其他进程收养
僵尸进程
- 子进程先于父进程结束,向父进程发送
SIGCHILD(17)
信号,但是父进程没有回收子进程, 此时子进程成为僵尸进程
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 1){
perror("fork");
return -1;
}
if(id == 0){
printf("child process:%u\n",id);
}
}
内存映射
int main()
{
char *p = mmap(NULL, 4096, PORT_READ|PORT_WRITE, MAP_SHARED|MAP_ANONYMOUS,0,0);
if(p == MAP_FAILURE){
perror("mmap");
return -1;
}
strcpy(p, "aaaaaaaaa");
pid_t id = fork();
if(id == -1){
perror("fork");
return -1;
}
if(id == 0){
strcpy(p, "hello");
sleep(2);
}
else{
sleep(1);
printf("%s\n", p);
}
return 0;
}
/*
结果上父进程输出的p是子进程的hello
*/
mmap是建立虚拟内存和物理内存的映射关系,在fork之后,子进程也会建立虚拟内存和物理内存的映射,父子进程相当于映射到同一块物理内存
退出
-
从main函数中执行return
-
#include <stdlib.h> void exit(int status);
-
#include <unistd.h> void _exit(int status); //c标准库中有一个相同功能的函数 #include <stdlib.h> void _Exit(int status);
-
进程的最后一个线程执行了返回语句
正常退出
-
#include <stdlib.h> void atexit(void (*function)(void)); int on_exit(void (*function)(int,void *),void *arg);
-
功能,用于注册函数,在该进程结束之前调用
如果注册了多个函数,则进程结束之前的调用顺序和注册的顺序正好相反on_exit 返回0表示注册成功 返回非0表示注册失败
atexit注册的函数没有返回值,且没有参数
on_exit注册时,除了需要给定注册的函数,还需要给定第二个参数arg,这个arg用于在调用注册函数时作为第function的第二个参数,function的第一个参数是进程退出状态或者返回值 -
冲刷并关闭所有任然处于打开状态的标注IO流
#include <stdio.h>
FILE *tmpfile(void);
- 建议使用EXIT_SUCCESS/EXIT_FAILURE常量宏来替换进程的退出状态和返回值提高平台的兼容性
异常退出
#kill -9 18922 #给18922进程发送9信号
sig [1,64] pid 指定的进程
#kill -l #信号列表
- 最后一个线程对pthead_cancel请求做出响应
wait/waitpid
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid,int *status,int options);
等待子进程终止将获取其终止状态,成功返回终止进程的进程号(进程ID),失败返回-1
wait
- wait 函数,用于等待子进程结束,如果没有子进程结束,则调用此函数会阻塞,直到有子进程结束为止
- 如果已经有子进程结束了,然后父进程调用wait会直接获取该结束的子进程的状态并返回子进程ID
- 如果多个子进程结束,则会选择任意一个子进程返回ID
- 如果没有子进程可等待了,则返回-1, 且errno设置为ECHILD
- wait函数参数可以为NULL, 表示不获取子进程的退出状态
- 在任何一个子进程结束前,调用wait函数只能阻塞调用进程,而waitpid有更多的选项
waitpid
pid取值
<-1 - 等待其组ID等于该参数绝对值的任一子进程,即等待属于特定进程组内的任一子进程
-1 - 等待任意一个子进程,此时效果等同于wait函数
>0 - 等待由pid参数所标识的唯一的特定的进程
0 - 等待其组ID等于调用进程组ID的任一子进程,即等待与调用进程同属于一个进程组内的任意子进程
options:
0 - 阻塞模式 等同于wait
WNOHANG - 非阻塞模式,若没有合适的结束的子进程,则返回0 只有在非阻塞时才会返回0
WUNTRACED - 若支持,且子进程处于暂停状态,则返回其状态 暂停 STOP
WCONTINUED - 若支持,且子进程暂停后继续运行,则返回其状态
status: 作为输出参数,用于获取进程的状态标识 包含1.进程是否正常退出 2.异常退出的信号 3.返回值
若取NULL,则表示不关心进程的状态
WIFEXITED(status) - 用于判断子进程是否是正常终止 如果正常退出则为真
如果子进程正常退出,则可以用 WEXITSTATUS(status)宏获取子进程return或者exit/_exit/_Exit函数所返回值的低8个二进制位
WIFSINgalED(status) - 用于判断子进程是否是异常终止,如果是则为真
如果子进程异常退出,则可以用 WTERMSIG(status)宏获取终止子进程的信号
WIFSTOPPED(status) - 若支持,可以判断子进程是否处于暂停状态
WIFCONTINUED(status) - 若支持,可以判断子进程是否在暂停之后继续运行
int main()
{
int i;
pid_t id[10];
for(int i = 0; i< 10; i++){
id[i] = fork();
if(id[i] == -1){
perror("fork");
return -1;
}
if(id[i] == 0){
printf("child process:%u\n", getpid());
sleep(i*i);
return i*i;
}
}
printf("father :%u, creat 10 child success!\n", getpid);
int s;
pid_t pid;// = waitpid(-1, &s, 0);
int opt = 0;
while(1){
printf("1. wait pid\n");
printf("2. try wait pid\n");
printf("3. wait any pid\n");
printf("4. try wait any pid\n");
int in;
scanf("%d",&in);
if(in == 1 || in == 3){
opt = 0;
}
else if(in == 2 || in == 4){
opt = WNOHANG;
}
if(in == 1 || in == 2){
printf("input pid:");
scanf("%u",&pid);
}
else{
pid = -1;
}
pid_t ret = waitpid(pid, &s, opt);
if(ret == -1){
if(errno == ECHILD){
printf("all over\n");
break;
}
perror("waitpid");
}
else{
printf("%u process over\n", getpid());
if(WIFEXITED(s)){
}
else if(WIFEXITED(s)){
}
}
}
}
exec系列函数
#include <unistd.h>
int execl(const char *path,const char *arg,...);
int execlp(const char *file,const char *arg,...);
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[]);
int execvpe(const char *file,char *const argv[],char *const envp[]);
int execve(const char *filename,char *const argv[],char *const envp[]);
-
带
e
的需要传递环境列表- 以数组形式传递环境列表,且其最后一个元素为NULL
-
带
v
的说明参数列表是以数组的形式传递,数组的最后一个元素为NULL -
带
l
的说明参数列表必须通过可变长参数列表传递 参数列表必须以NULL为结尾 -
带
p
的若file中不包含"/",则将其视为文件名,根据PATH环境变量进行搜索该文件- 没有p时,可执行文件需要带路径,否则只会从当前目录下查找
-
exec函数若执行成功,不会返回,失败返回-1
-
exec函数基本上用于
vfork
创建的子进程-
vfork
创建的子进程不会拷贝父进程的地址空间
-
system
#include <stdlib.h>
int system(const char *command);
- 标准C语言函数,执行command命令,成功返回command对应进程的终止状态,失败返回-1
- 测试shell是否可用 ,当command取NULL时,返回非0表示shell可用,返回0表示shell不可用
- 该函数的实现fork+exec+waitpid
- system函数对各种错误和信号都做了必要的处理
信号
kill -9 pid
- 子进程结束后会给父进程发送SIGCHILD(17)信号
- ctrl+c
- ctrl+\
概念
是一种软件中断
- 中断
- 中止正在执行的程序,转而执行其他任务
- 其他任务结束后可继续执行
- 中止正在执行的程序,转而执行其他任务
-
- 软件中断
- 程序中断
- 软件中断
-
- 硬件中断
- 来自硬件的中断
- 硬件中断
信号提供了一种异步执行任务的机制
- 列出信号 kill -l
信号 | 解释 | 产生条件 | 默认动作 |
---|---|---|---|
SIGINT(2) | 终端中断符信号 | ctrl+c | 终止 |
SIGQUIT(3) | 终端退出符信号 | ctrl+\ | 终止+core文件 |
SIGABRT(6) | 异常终止信号 | abort() | 终止+core文件 |
SIGBUS(7) | 总线错误信号 | 硬件故障(内存故障) | 终止+core文件 |
SIGKILL(9) | 终止信号 | 不能被捕获和忽略,用于杀死进程 | 终止 |
SIGCHLD(17) | 子进程改变状态信号 | 子进程终止时,向父进程发送 | 忽略 |
SIGTSTP(20) | 终端停止信号 | ctrl+z,发送给前台进程组中所有进程 | 停止进程 |
# jobs 可以查看当前终端后台任务
# fg [jobs编号] 可以把后台执行任务调到前台执行
# cmd & 后台执行 不占用终端 终端就可以继续交互
分类
- 可靠信号
- 不可靠信号
来源
- 硬件异常,除0、对0取余、无效内存访问
- 这些异常会被硬件设备检测到,并通知系统内核
- 系统内核向引发异常的进程递送相应的信号
- 软件异常
- kill/alarm/sigqueue/settimer函数都会产生信号
处理
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
/*
给signum信号事先注册信号处理函数handler
返回值:
失败返回 SIG_ERR
成功返回 信号之前的处理函数
当成功注册一个信号的处理函数时,程序会继续往下执行,但是当程序接收到该信号时,程序会中断正在执行的代码,转而执行注册的信号处理函数handler,执行完handler之后程序继续回到中断的位置继续执行
参数:
signum - 要给哪个信号注册信号信处函数 可以使用整数[1,64] 可以使用宏 SIGINT SIGQUIT
SIGSTOP SIGCONT
handler - 信号处理函数指针
SIG_IGN 表示直接忽略该信号
SIG_DFL 使用默认处理方式
某些信号不能被捕获和处理,所以不能为这些信号注册信号处理函数,如 SIGKILL 9
在某些unix系统上,通过signal注册的信号处理函数只有一次有效,即调用完信号处理函数之后,就恢复成默认的处理方式,为了获得持久有效信号处理,需要在信号处理函数中再次调用singal函数进行重新注册
*/
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。