进程
进程的定义
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,也是操作系统结构的基础。
例如当QQ程序运行的时候,计算机会先从磁盘读取QQ程序到内存,然后OS管理这个程序,这就是进程。简单来说进程就是OS处理进程的数据结构加代码与数据。一般在Linux中这个数据结构就是task_struct结构体,本质就是单链表。如下代码就是一个进程结构体,里面有进程的状态,ID,分配的内存等。
struct task_struct {
// 进程状态,例如:TASK_RUNNING, TASK_INTERRUPTIBLE 等
volatile long state;
// 进程的进程号(PID)
pid_t pid;
// 指向下个进程的 task_struct 指针
struct task_struct *next;
// 进程的进程组号
pid_t tgid;
// 进程的优先级
int prio;
// 进程的虚拟内存描述符
struct mm_struct *mm;
// 进程的内核栈指针
void *stack;
// 进程的可执行文件信息
struct files_struct *files;
// 进程的文件系统信息
struct fs_struct *fs;
// 进程的信号处理相关信息
struct signal_struct *signal;
// 进程的命名空间信息
struct nsproxy *nsproxy;
// 进程的调度信息
struct sched_entity se;
// 进程的运行时间统计
u64 utime, stime;
// 进程的启动时间
unsigned long long start_time;
// 进程的命令行参数
char comm[TASK_COMM_LEN];
};
进程的目的与意义
一般在电脑,手机上,我们不止要运行一个程序,会同时运行qq,杀毒软件,浏览器等。如果我们不先描述进程的属性定义一个结构体,进程之间会产生干扰,那么OS就无法较好的管理内存。计算机就会是不是蓝屏,这对于用户来说十分的不友好。
进程采用单链表的原因
在使用QQ的时候,我们会打开他,不用的时候可以关闭它,或者让他保持后台运行,因此对于进程而言,就有大量的增删操作,如果我们采用数组的方式就会造成数据处理慢,也就是使用起来变卡了。
其次使用数组就必须提前分配一大段连续的地址空间,但可能OS不需要如此大的空间,就会造成资源浪费,其次在数组扩容的时候,计算机可能没有足够大的连续空间,就会造成内存不够。采用单链表的方式就更加的灵活,充分利用散落空间。
进程就是运行的程序,在Linux中被描述为task_struct结构体,用单链表进行管理。可以通过XShell来访问服务器.ps ajx命令来查看进程,如下图
1.进程相关的函数
1.1getpid()
man getpid
在Linux中可以用上述命令查询该函数。
在操作系统中有多个进程,为了区别他们,就引入了类似于身份证号的概念,每个进程都有自己唯一的pid(process Identification),当程序运行起来时就是一个进程,因此可以在程序中获取当前pid然后打印出来。
#include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7
8 pid_t id=getpid();
9
10 printf("我的ID是%d\n",id);
11
12
13 return 0;
14 }
~
运行程序,便可以得到如下ID。
1.2getppid()
man getppid
在Linux中可以用上述命令查询该函数。如下图
在Linux中-bash就是命令行程序,我们输入的ls,mkdir命令在被bash解析后都会转化为一个进程,此时bash就是父进程,ls就是子进程,在子进程内调用getppid就可以得到父进程的pid。
当我们打开多个命令行时便会有多个bash,如下图。
我们可以通过如下例子证明任何进程(bash除外)是由父进程创造的,
ps ajx | head -1 && ps ajx | grep bash
上述命令可以分为两个命令,用&& 连接,ps ajx | head -1 ,表示显示ps ajx结果的第一行,也就是状态栏。
ps ajx | grep bash表示在进程信息中查询有关键词bash的,此时会有两个符合条件,第二个时命令行bash进程自然可以被检索到,而第一个仔细看其实是grep进程。grep是我们在命令行输入的指令,在经过bash解析后生成一个进程。此时grep的父进程ID就是32095,恰好就是bash的ID。
1.3 fork
既然进程是由父进程创造的,那我们平时写的test.c文件在编译运行后也是一个进程,就可以由这个进程在创造出子进程,Linux就提供了fork函数建立子进程。
在man介绍中返回值最重要,如下图
成功时,在父进程中会返回子进程的进程 ID(PID),而在子进程中会返回 0。失败时,在父进程中返回 -1,不会创建子进程,并且会适当地设置 errno。
于是我们便可以通过下属例子证明子进程是由父进程建立的。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
pid_t id=fork();
if(id == -1)
{
printf("子进程创建失败\n");
}
if(id == 0)
{
//子进程
while(1)
{
printf("我是子进程,PID为%d,PPID为%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//父进程
while(1)
{
printf("我是父进程,PID为%d,子进程ID为%d\n",getpid(),id);
sleep(1);
}
}
return 0;
}
1.4结束进程
在Linux中可以使用ctrl+c结束进程,如下图。按下ctrl+c后进程就结束了
也可以使用kill -9 +ID结束进程。其中-9表示结束信号,
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
如下图使用kill -9在新开的命令行中结束父进程
2.进程属性
在Linux中为了方便管理进程,就设置了多种属性,有如下状态。
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
Linux要管理进程就必须遵循一个原则先描述再组织,即将进程各种属性封装再结构体内部,在通过队列组织起来,这样对于进程的管理就转化为对于数据结构的管理。在Linux中通常以固定的时间片来刷新进程,即Linux为了保证每个进程公平,当一个进程运行固定的时间后就切换到下个进程,依次循环往复。对应于队列就是头删尾插。
2.1 R(运行状态)
运行状态实际上就是进程正在内存中的运行队列中,此时进程就在CPU中不断切换运行。
1 #include<stdio.h>
2
3 int main()
4 {
5 while(1)
6 {
9 }
10 return 0;
11 }
让上述进程进入到死循环中,就会一直运行,再查看进程就可以看到R(运行状态)。此时proc进程就一直在内存中的运行队列中。
2.2 S(休眠状态)
根据之前的冯诺依曼架构可以知道,CPU只与内存进行数据交换,由于磁盘等输出设备读取速度相对于内存过慢,CPU不会与输出设备例如显示器,磁盘,进行数据交换,而是要通过内存间接交换。
当进程要与外部设备输出时,这个消耗的时间对于CPU来说时巨大的,为了避免资源的浪费,进程在Linux中就是一个task_struct结构体,可以放在任何task_struct队列中,于是便将进程从内存的运行队列中取出换到设备的等待队列中等待数据交换完毕。。此时进程的属性就是休眠状态。如下例子
#include<stdio.h>
2
3 int main()
4 {
5 while(1)
6 {
7 printf("我是一个进程\n");
8
9 }
10 return 0;
11 }
在上述的代码中仅仅加入一条打印语句,进程就处于S休眠状态,说明此时进程被移动到显示器的等待队列中了。但这个程序是个死循环,一定会有再次加载到CPU中的时候,也就是R状态,但是由于CPU运行速度十分快,刚好查询到R状态概率十分小。
for i in $(seq 1 1000); do ps ajx | head -1 && ps ajx | grep ‘<proc>’ | grep -v grep; done
运行上述脚本,在尝试数分钟后终于也是找到了那一瞬间为R的状态。
2.3 D(深度休眠状态)
这个状态十分少见,仅在系统濒临崩溃的边缘才可以看见。假如现在进程A里存储了1000W人口信息资料,此时要保存到磁盘中,那么给他状态设置为S,此时OS(操作系统)在管理进程,内存严重的不足了,OS的使命是保护系统正常运行,他看见A进程是S状态,又占用大量的内存,就把A进程强制结束了(就像手机打开应用闪退一样)。此时如果磁盘正常存储结束也就没什么问题,但是D磁盘内存也可能不足,此时磁盘向进程A返回错误信息,发现A进程没了,直接人傻了。十分重要的数据就发生了丢失,这是决定不允许的。
于是便引入了D状态(磁盘休眠/深度休眠)。==系统可以崩溃,但是数据不可以丢失。==二者取其轻,选择相对可以接受的选择。
2.4 T(停止状态)
在Linux中可以使用kill向进程发送信号。如下图
其中19号信息就可以暂停进程。
kill -18 16394
可以再次启动进程
2.5 t(追踪停止状态)
在Linux中经常会使用gdb进行调试,虽然不怎么好用哈。当我们在程序中打断点,并且r运行到断点时,此时程序就是t状态。
gcc -g -o test test.c
gdb test
2.6 X(死亡状态)
死亡状态就是进程彻底结束,资源全部释放的时候,是一瞬间的状态,不能被检测到。
2.7 Z(僵尸状态)
僵尸状态就是进程没有完全释放的状态,但是进程不在运行了。进程可以理解为是内核数据结构(task_struct)+代码和数据,当子进程完成任务时,就会先释放代码加数据,但保留task_struct供父进程判断检测子进程是否完成任务。死亡状态也就是将最后的tast_struct也释放完毕的瞬间状态。
ls对于命令行解释器bash来说是其的子进程,当ls进程执行完时,返回的退出状态可以用==echo ¥?==显示出来,如下图。
ls执行错误时也会返回对应的错误码,然后bash根据错误码打印对应信息。
如果僵尸进程不被父进程管理回收那么他将永远是僵尸进程,如果父进程不管理僵尸进程的话,由此便有可能造成内存泄漏,也可以通过下面例子认识。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
pid_t id=fork();
if(id == -1)
{
printf("子进程创建失败\n");
}
if(id == 0)
{
//子进程
while(1)
{
printf("我是子进程,PID为%d,PPID为%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//父进程
while(1)
{
printf("我是父进程,PID为%d,子进程ID为%d\n",getpid(),id);
sleep(1);
}
}
return 0;
}
运行上述程序并且使用kill结束子进程,但此时父进程没有做任何处理,那么子进程就是僵尸状态,如果父进程一直不处理那么就会造成内存泄漏。
刚才是父进程和子进程都存在的情况下,结束子进程那么子进程是僵尸进程。如果结束父进程的话那么子进程就会操作系统领养称之为孤儿进程。操作系统的pid为一。
3 进程优先级
3.1 进程优先级意义
CPU的资源具有稀缺性,进程优先级是进程获得CPU资源的一种先后顺序,即竞争力,就像我们下课时去食堂排队打饭一样,先到的他的优先级就越高就越先打到饭吃饭。
3.2 修改优先级
Linux优先级由两部分组成一部分是priority(默认优先级),一部分是nice(微调优先级),进程的真实优先级等于priority+nice。在Linux中修改进程的优先级只能修改nice不能修改默认优先级。
3.2.1 top修改优先级
按照以下步骤操作修改,top -> r ->pid -> nice值
需要注意的是普通用户禁止频繁修改优先级, 如果被系统弹出禁止,需要切换到超级用户或者用sudo执行top。
在这里需要注意的是nice的值的范围是从-20到19,输入超过这个范围的值会自动适配到这个范围内。并且这个priority优先级是最初的优先级加上修改后的nice,而不是上一次的priority加上nice值。
3.2.2 命令行修改优先级
renice <nice值> -p <进程ID>
也可以修改为负数。在原来八十的优先级上减十就得到了现在七十的优先级.