1.通过系统调用获取进程标示符
进程id(PID)
父进程id(PPID)
每一个可执行程序运行起来之后都会成为一个进程,每个进程都有一个自己的id,以及一个父进程id,父进程就是创建自己进程的进程,每个进程都是一个执行起来的程序,所以肯定在这个程序中创建另一个程序,就是自己的子进程。
使用getpid这个函数就可以查看到自己这个进程的id,使用getppid这个函数就可以查看到父进程的id,getpid是一个系统调用函数,需要注意的是一个子进程只有对应的一个父进程,但是一个父进程可以有多个子进程。
进程每一次被启动时对应的pid都不一样,但是父进程的pid永远不会变。
cpp
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main()
{
pid_t id=getpid();
while(1)
{
cout<<id<<endl;
sleep(1);
}
return 0;
}
cpp
int main()
{
pid_t id=getpid();
pid_t pid=getppid();
cout<<"Im child: "<<id<<endl;
cout<<"Im father: "<<pid<<endl;
return 0;
}
\
2.通过系统调用创建进程-fork初识
这个是fork函数的介绍,需要包含两个头文件,fork函数的返回值是pid_t,并且fork函数有两个返回值。
我们来做一个小测试,看看返回值到底是怎么回事。
cpp
#include <unistd.h>
#include <sys/types.h>
#include <iostream>
using namespace std;
int main()
{
cout << "before fork:Im a process, pid:" << getpid() << ", ppid:" << getppid()<<endl;
fork();
cout << "after fork:Im a process, pid:" << getpid() << ", ppid:" << getppid()<<endl;
sleep(2);
return 0;
}
可以看到fork之后的代码被执行了两次,也就是从一个进程变成了两个进程,第三个是子进程,它的ppid就是父进程的pid。
那么我们怎么知道哪个是子进程,哪个是父进程呢?可以通过fork的返回值来判断,如果fork成功则对子进程返回0,对父进程返回子进程的id。此时就可以使用if来分流,让父子进程做不一样的事情。
我们都知道进程=内核数据结构+可执行代码和数据,那么子进程的代码和数据是什么呢?它是怎么运行起来的,实际上父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝) 。但是进程是具有独立性的,互相之间是不影响的。
3.阻塞状态
大家还需要知道的是,一个cpu只有一个运行队列,那么有些进程在等待硬件资源的时候,就会被os从运行队列中拿出来放到对应的硬件的队列中,并且改成阻塞状态,因为cpu也属于硬件,硬件就有自己的结构体,里面就有对应的队列。
那么当我们用户从硬件中输入数据的时候,这个进程就会被从硬件的队列中拿出来链入到cpu的运行队列,并且将进程状态改为运行状态。
4.阻塞挂起状态
挂起状态通常伴随着阻塞,这个状态的前提是计算机的资源比较吃紧,这个时候os就会将这个进程的代码和数据写入到外设当中,当资源足够时再拿过来,写入的这个过程就是在腾空间。这个写入的本质就是用时间换空间,宁愿让进程慢点,也不要让os挂掉。
5.进程状态
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 */ };
R运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
进程大多数情况下是大量的在运行,那么os怎么管理这些进程呢?就需要先描述再组织,描述就是数据结构+可执行代码和数据,那么组织就需要使用队列来对进程进行排队,因为大部分进程并不是一直在运行,有些进程也可能在等待资源 ,比如等待我们从键盘输入,所以进程有许多种状态。
那么进程的状态决定了上面呢?决定了后序的动作,那么动作的先后就需要进行排队,每一个cpu都有一个自己的运行队列,那么只有进程被放入了这个运行队列,就都是运行状态,就算在排队也是运行状态。
5.1睡眠状态
睡眠状态也就是进程在等待事件的完成,那么为什么执行了一下的代码,在查询该进程信息的时候,会是睡眠状态呢?这个进程不是一直在运行吗?当我们去掉sleep的时候,这个进程还是s状态,其实是因为cout这个函数的本质是向显示器进行打印,显示器是在我们面前,可是这个可执行程序是在远端的云服务器上运行的,cpu比外设要快很多,所以注定了cout的大部分时间是在等待着执行。
cpp
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main()
{
while(1)
{
cout<<"Im a process, pid: "<<getpid()<<endl;
sleep(1);
}
return 0;
}
当我们去掉cout,死循环里面什么都不做,就能查到是运行状态。
进程的睡眠状态其实就是os的阻塞状态,这里的睡眠有时候也叫做可中断睡眠,因为可以用ctrl+c来终止。
5.2停止状态
当我们使用kill -19 这条命令就可以使一个进程停止,也就是进入停止状态。
如果我们还想让这个进程跑起来,可以使用kill -18这个命令。
5.3僵尸状态
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
我们创建进程就是为了给我们完成某件事,那么这个进程退出时就需要给我们返回结果,一个进程在退出时可以释放掉代码和数据,因为都没有用了,但是需要先保存一下pcb,因为需要被os或者其他进程获取到该进程的退出信息,我们把一个进程已经退出但并没有被获取退出信息的状态称为僵尸状态。
下面这段代码就是前5秒父子进程同时运行。后面5秒父进程单独运行,在第5秒时子进程会被强制退出,那么就进入了僵尸状态Z。 为什么要有Z状态呢?我们创建进程就是为了将这个进程完成某个工作,
cpp
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt)
{
cout << "Im child, pid: " << getpid() << ", ppid: " << getppid() << endl;
sleep(1);
cnt--;
}
exit(0);
}
int cnt = 10;
while (cnt)
{
cnt--;
cout << "Im father, pid: " << getpid() << ", ppid: " << getppid() << endl;
sleep(1);
}
wait(NULL);
cout<<"father wait child success....."<<endl;
return 0;
}
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
内存泄漏?是的!
5.4孤儿状态
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为"孤儿进程"
孤儿进程被1号init进程领养,当然要有init进程回收喽。
当父进程退出之后,子进程还一直在运行,这时子进程就会变成孤儿进程, 它的ppid也变成了1,也就是被1号进程领养了,被回收了,从s+变成了s,也就是变成了后台进程,此时只能使用kill -9这个命令来杀掉。
cpp
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 100;
while (cnt)
{
cout << "Im child, pid: " << getpid() << ", ppid: " << getppid() << endl;
sleep(1);
cnt--;
}
exit(0);
}
int cnt = 10;
while (cnt)
{
cnt--;
cout << "Im father, pid: " << getpid() << ", ppid: " << getppid() << endl;
sleep(1);
}
// wait(NULL);
// cout<<"father wait child success....."<<endl;
return 0;
}
6.进程优先级
6.1基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能.。
在一台计算机中,资源绝对是占多数,硬件绝对是占少数,所以要合理的安排进程,就需要给它们设置优先级。
6.2查看系统进程
在linux或者unix系统中,用ps --l命令则会类似输出以下几个内容:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
6.3 PRI and NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序 ,此值越小进程的优先级别越高,PRI的范围是60-99,进程的默认PRI是80
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别
6.4 PRI vs NI
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正修正数据
6.5查看进程优先级的命令
6.5.1用top命令更改已存在进程的nice:
top
进入top后按"r"-->输 入进程PID-->输入nice值
7.其他概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
今天的分享到这里就结束啦,感谢大家的阅读!