进程与程序区别
进程 | 程序 | |
---|---|---|
状态 | 程序运行动态体现 | 静态的可执行文件 |
Windos 下可执行文件类型是exe ;Linux 下可执行文件类型是ELF ,且无固定后缀 | ||
系统管理的最小单位 | 进程调度的最小单位 |
`ELF`格式文件类型:可执行文件、可重定位文件、共享目标文件、核心转储文件
`readelf -h a.out`:查看可执行文件头部信息(文件创建之初就装有本身的相关属性,不仅仅只有可执行代码)
`task_struct`:对于进程来说比较重要的结构体,目前最大的结构体,包含所有进程信息
进程组织形式
init
属于整个系统中最顶层的进程(没有父进程),其余进程都有父进程(若生父进程先被销毁,销毁时父进程的父进程会打印一句话,子进程会被系统派一个进程作为“养父”,二养父不具备打印这句话的功能),所以有的进程会显示有两个父进程;
pstree
:查看系统进程树
ps -ef
:查看系统当前进程
如下:
UID
:所属用户
PID
:类似于身份证(具有唯一性)
PPID
:父进程的PID
,可以相同是代表多个进程可以从属一个父进程
TTY
:从属于哪一个控制台,若是?则是系统的守护进程
top -H
:动态查看系统当前进程
htop
:动态查看系统当前进程(需要自行安装插件)
//sudo apt-get install htop
进程的基础
要求:能用自己的话语表述完整流程
睡眠态、挂起态:缺失某些资源导致不可执行(前者是可响应信号后者可理解为深度睡眠)
暂停态:并没有到就绪队列中,而在等待命令(用户主动暂停)
僵尸态:进程被杀死,但是尸体还在,存在信息残留(什么原因被杀死、什么时候被杀死这些信息被封存在实体中)
死亡态:不存在任何信息残留
以下函数作了解部分,需要知道相关原理:
函数-fork
#include <unistd.h>
/*
功能:创建一个新进程
返回值:成功返回0或大于0的正整数 失败返回-1
特别注意:
1、成功 在子进程中返回0,父进程中返回子进程的PID(也就是一个大于0的正整数),所以返回值有两个
2、这就类似于“细胞分裂”,父子进程几乎一样,但父子进程的PID、记录锁、挂起的信号不同;并且位于两个不同的空间,相互独立(地址一样) 虚拟内存的地址相同,但是映射的物理地址不同
对于一个变量,父子进程操作一个变量时,二者变量互不影响,虚拟地址相同,物理地址不同
对于一个文件,父子进程读取一个文件时,若子进程后读取,会接着父进程读取位置继续读取
这是由于对战数据会受到当前程序的影响,而文件的存在通常不受挡墙程序影响
3、子进程必定会从fork返回值的下一句逻辑语句执行,否则会产生无限子孙
4、父子进程的运行顺序是随机的,没有固定运行先后顺序
*/
pid_t fork(void);
案例
int main(int argc, char* arhv[])
{
pid_t pid = fork();
if(pid < 0)
{
perror("fork fail");
return -1;
}
if(pid == 0)
{
printf("Son\n");
}
if(pid > 0)
{
printf("Father son_pid = %d\n", pid);
}
return 0;
}
函数-exec函数族
#include <unistd.h>
/*
功能:进程中加载新的程序文件
参数1:待加载文件路径(可以使用 whereis ls 来查看 ls 的路径)
参数2:待加载文件名字
grg:以列表的方式罗列参数
argv:以数组的方式罗列参数
返回值:成功不返回 失败返回-1
特别注意:
1、被加载文件的参数列表需要以自身名字为开始,以NULL结尾
2、执行成功后续的代码将不执行
*/
int execl(const char* path, const char* arg,...);
int execv(const char* path, char* const argv[]);
案例
int main(int argc, char* argv[])
{
int ret = execl("/bin/ls", "ls", "l", NULL);
//execlp("ls", "ls", "-l", NULL);
/*
char* argv[] = {"ls", "-l", NULL};
execv("/bin/ls", arg);
//execvp("ls", arg);
*/
if(ret == -1)
{
//只有执行失败才会返回
perror("execl error");
}
else //一旦exec族函数被执行成功,后面的代码将不会执行
{
printf("execl ok!\n");
}
return 0;
}
函数-exit/_exit
#include <unistd.h>
#include <stdlib.h>
/*
功能:结束本进程
参数:子进程退出值(正常为0,异常非0)
返回值:不返回
*/
void _exit(int status);
void exit(int status);
案例
void fun1()
{
printf("fun1\n");
}
void fun2()
{
printf("fun2\n");
}
int main()
{
//atexit函数用于注册函数,并不会真正执行被注册函数
//当进程退出时,被注册函数才会退出
//执行顺序符合栈规则,先注册fun1,后注册fun2,所以先结束进程fun2,后结束进程fun1
atexit(fun1);
atexit(fun2);
//exit 可以清理缓冲区(输出缓冲区内容) 用于结束当前进程,区别于return
//exit(0);
//_exit 不会清理缓冲区
_exit(0);
//return 会清理缓冲区 用于结束当前函数,在哪个函数就结束哪个函数
//return 0;
}
缓冲区清理五种情况:
1、return
2、exit
3、"\n"
4、缓冲区已满
5、手动清理"fflush"
函数-wait/waitpid
#inlcude <sys/wait.h>
/*
功能:等待子进程
sata_loc:子进程退出状态
pid:
option:
返回值:失败返回-1
*/
pid_t wait(int* sata_loc);
pid_t waitpid(pid_t pid, int* stat_loc, int options);
案例
int main()
{
pit_t pid = fork();
if(pid < 0)
{
perror("fork error");
return -1;
}
//子进程
if(pid == 0)
{
printf("sub pid = %d, ppid = %d\n", getpid(), getppid());
/*
return 255;
exit(88);
*/
}
//父进程
if(pid > 0)
{
sleep(1); //若不写这一句话,那么子进程后执行后退出,那么父进程先结束,子进程就变成孤儿进程
printf("sub pid = %d, ppid = %d\n", getpid(), getppid()); //getpid:获得当前进程pid
int stat;
pit_t ret = wait(&stat);//wait阻塞等待(会一直等到子进程结束,若子进程不结束,那么会影响父进程执行)waitpid非阻塞等待,若子进程未结束,也不影响父进程执行,并且获取不到子进程pid,返回0,告知子进程还未结束(相较于wait多一种情况)
if(ret < 0)
{
perror("wait error");
}
else
{
printf("exit sub pid = %d,exit value = %d\n", ret, stat);
}
}
return 0;
}
精灵进程
进程组
-
每运行一个程序就会有一个进程,进程组就是一组进程(便于多个关联进程的管理),拥有一个组ID(通常使用进程组中所有
PID
最小的); -
组长进程若先退出不会有新进程组长,且会等到进程组全部成员退出,进程组才会消亡;
-
若进程成员创建子进程,同样子进程会和父进程在同一组;
-
进程必定属于某一个进程组,且只能属于一个进程组;
对话期
-
Linux是多用户多任务操作系统;
-
用户登录一次系统就会形成一个会话,一个会话只有一个领导和一个前台进程组还可以有一个控制终端,可以拥有多个进程组(会话包含控制进程(会话首进程)、一个前台进程组、任意个后台进程)
终端
-
Linux通常说的终端就是于用户交互的命令窗口
-
精灵进程又叫守护进程/后台进程,若要关闭后台进程则只能关闭系统,而前台进程可以受到多个影响,如关闭终端、
Ctrl C
、关闭终端;./a.out &
这就是一个最基础的精灵进程
精灵进程(了解)
创建流程:
-
先创建一个进程
p1
,这个p1
会变成进程组组长; -
然后利用
p1
创建其子进程p2
,然后需要干掉p1
,p2
会成为会话期的组长; -
然后利用
p2
创建其子进程p3
,然后需要干掉p2
; -
最后
p3
再去隐藏文件掩码等威胁自己存在的信息;