一.task_ struct内容分类
标示符(pid): 描述本进程的唯一标示符,用来区别其他进程; 状态(status): 任务状态,退出代码,退出信号等; 优先级(PRI): 相对于其他进程的优先级; 程序计数器: 程序中即将被执行的下一条指令的地址; 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器; I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表; 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
在抢占式多任务处理中,进程被抢占时,所有cpu寄存器的内容,页表指针,程序计数器会被保存下来。
二.通过系统调用获取标识符
linux中可以通过 系统调用接口:getpid 获取该进程的PID,getppid可以获取父进程的PID
例:
代码语言:javascript
复制
#include <stdio.h>
#include <unisted.h>
int main()
{
while(1)
{
printf("我是一个进程 pid: %d ppid: %d\n",getpid(),getppid()); //获取进程的pid和
//其父进程的pid
sleep(1);
}
return 0;
}
三.fork函数的认识与理解
fork认识
linux输入 man fork 可以看到以上信息:
1.fork包含在头文件 <unisted.h> 中;
2.返回值是 pid_t (这个 pid_t 是有符号整型);
3.作用是创建一个新的进程;
4.当fork调用成功时会返回0给子进程,返回子进程的 pid 给父进程;
当fork调用失败时返回一个负值;
几个问题:
A.为什么要返回两个值?
B.一个函数怎么可以有两个返回值?
C.一个变量怎么会有两个不同的内容?
A:
fork多创建了一个进程, 返回两个值是为了区分不同的执行流,执行不同的代码块;
B:
其实fork之后的代码是父子进程共享的,fork函数既然是函数,且有返回值,那么内部一定有return 语句,一般一个函数执行到return时,那么就意味着它的核心任务完成了,要准备返回了,那return语句是代码吗?答案当然是的!既然父子进程代码是共享的,那么return也会被执行两次,所以fork函数就有两个返回值。
C:
我们知道:进程=PCB+代码和数据
fork创建的子进程的PCB里的内容其实和父进程的大部分是相同的,但是子进程只有PCB是不行的,子进程和父进程共享代码,那数据呢?一般情况下,子进程和父进程也是共享数据的,但是一直共享数据也不现实,因为当我们要修改数据时,会把两个进程的数据都改了,这并不是我们想要的,但是重新开一块空间拷贝父进程的数据又有点浪费,所以linux就使用了一种叫写时拷贝的技术。
即在要修改数据时,给这个要修改的数据单独拷贝一份,你要修改多少就拷贝多少,这样就解决了上面的问题。
那么 return 一个值算是对数据的修改吗?当然算!return 会写入数据,也就是修改了数据,所以一个变量会有两个不同的内容。
示例
我们可以使用 ps 指令 观察
代码语言:javascript
复制
ps ajx |head -1&&ps ajx|grep mypro
代码语言:javascript
复制
int main()
{
printf("我是一个进程\n");
pid_t id =fork();
if(id==0)
{
//子进程
while(1)
{
printf("我是子进程 pid: %d ppid: %d\n",getpid(),getppid( ));
sleep(1);
}
}
else if(id>0)
{
//父进程
while(1)
{
printf("我是父进程 pid: %d ppid: %d\n",getpid(),getppid ());
sleep(1);
}
}
else
{
//error
}
return 0;
}
其实先执行父进程还是子进程这跟你的调度器有关。
我们再来看看父进程的父进程是谁:
我们发现,父进程的父进程是bash进程,bash进程就是我们的命令行解释器。
三.进程状态
操作系统学科的状态
我们先来认识以下操作系统学科上的状态:运行,阻塞,挂起
运行:
其实内存中有一个叫运行队列的结构体,凡是放在这里面的进程,都处于运行状态;运行队 列里的PCB之间使用双链表组织起来,所以运行队列中分别有一个 head指针指向双链表的 头,一个tail指针指向双链表的尾。
一个进程把自己放到CPU上就开始运行了,但不会一直运行,有一个时间片的概念,它规定 了一个进程在CPU上运行的时间,当超过这个时间时,这个进程就会被拿下来,大量的从 CPU上拿下进程,运行进程,这个称为进程切换。
阻塞:
阻塞可以说是处于一种等待的状态,大多会涉及到外设,外设的速度是毫秒级的,CPU的速 度是纳秒级的,相差6个数量级,所以一般涉及外设的访问,大多数会处在阻塞状态;
处于阻塞状态的进程会被放到等待队列中,需要注意的是,内存中有非常多的等待队列,而 运行队列只有一个(有几个CPU就有几个运行队列)。
挂起:
当内存严重不足时,系统会把一些进程的代码和数据换出到外设中(通常是磁盘),只留 PCB在内存中,需要的时候再把代码和数据换入到内存中,处于此状态的进程称为挂起状 态。
所以重装系统的时候,除了会看到C盘,D盘什么的,还会看到一个叫swap的分区,这个就 是交换分区。
linux中进程的状态
linux中的进程状态分为这几种
代码语言:javascript
复制
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 */
};
这个S状态和D状态就算是阻塞状态了,S状态又叫浅度睡眠,D状态又叫深度睡眠,处于深度睡眠的进程不会响应任何请求,你只能慢慢等它结束,或是断电。
下面通过一些代码演示
代码语言:javascript
复制
int main()
{
while(1)
{
printf("我是一个进程 pid: %d ppid: %d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
可以看到,使用printf需要访问外设,此时进程是处在睡眠状态的;
这个 + 号表示是在前台运行,没有 + 号就是在后台运行,后台运行的进程只能使用 kill 命令发送 -9 信号加进程的PID杀掉这个进程。
代码语言:javascript
复制
kill -9 PID
kill -l 可以查看kill 都可以发送哪些信号
其中,-19 信号可以暂停一个进程,即让进程处于T状态,-18可以继续这个进程,但此时该进程就在后台运行了,要用 - 9 信号才能杀掉它;
僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程; 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态;
僵尸进程会一直占用系统资源,还会导致内存泄漏,所以要尽量避免僵尸进程。
孤儿进程
如果父进程先退出,子进程就称之为“孤儿进程”,此时孤儿进程在后台运行; 孤儿进程会被1号init进程领养,最后由init进程回收; 孤儿进程退出不会成为僵尸进程,因此也不会资源泄露。
守护进程&精灵进程
这两种是同一种进程的不同翻译,是特殊的孤儿进程,不但运行在后台,最主要的是脱离了与终端和登录会话的所有联系,也就是默默的运行在后台不想受到任何影响 。