引言:
根据冯诺依曼体系结构,我们现在的计算机的基本组成一般都是由输入/输出设备,cpu,内存(存储器)这些结构进行构成,对于我们平时写的代码来说,代码是不能被计算机进行理解的,所以我们出现编译器,编译器通过将代码翻译成为将我们的代码翻译成为二进制文件,这样我们的计算机才可以理解.
进程的基本概念:
1.官方下面的进程的基本的概念:
进程是程序一次执行的过程
2.Linux操作系统下面进程具体的概念:
进程是针对操作系统引出的一个概念,但是如果想要深入理解进程,就需要我们针对某一种主流的操作系统进行理解和学习,所以,由于Linux操作系统的开源性,今天我们就Linux操作系统来谈进程的相关的概念.
进程具体在Linux下面的实现方式:
task_block(Linux下面的进程管理模块),也可以说是进程的户口本
操作系统系统其实本质上也是一个进程,他是在我们电脑开机的时候自动启动的进程,我们后续的进程都是依托于操作系统进行创建和执行:
先描述,在组织,在平时我们写代码的时候其实经常用到这一个思想,就C语言来谈(当然目前主流的操作系统都是由C语言进行编写),为什么C语言中存在结构体这种结构,这是一种面向对象的编程的思想,我们想要描述一个实物,但是实物有着很多的属性,因此,在这种情况下,我们就会依赖于结构体对这种实物进行描述,无论是c++中的class还是python中的类,其实这种思想都是依托于面向对象的编程, 回到我们的前面提到的先描述,再组织,一个进程加载进入内存,我们的操作系统应该如何对进程进行管理呢,这个时候,我们就需要通过内核数据结构对我们的进程的属性等多种的特性进行描述.
在Linux操作系统中,我们描述进程的结构体叫做tast_struct,对于所有操作系统而言,这种结构我们统一称作pcb(process control block(进程管理模块)),
struct task_struct
{
/*
1. state: 进程执行时,它会根据具体情况改变状态。进程状态是进程调度和对换的依据。Linux中的进程主要有如下状态:
1) TASK_RUNNING: 可运行
处于这种状态的进程,只有两种状态:
1.1) 正在运行
正在运行的进程就是当前进程(由current所指向的进程)
1.2) 正准备运行
准备运行的进程只要得到CPU就可以立即投入运行,CPU是这些进程唯一等待的系统资源,系统中有一个运行队列(run_queue),用来容纳所有处于可运行状态的进程,调度程序执行时,从中选择一个进程投入运行
2) TASK_INTERRUPTIBLE: 可中断的等待状态,是针对等待某事件或其他资源的睡眠进程设置的,在内核发送信号给该进程表明事件已经发生时,进程状态变为TASK_RUNNING,它只要调度器选中该进程即可恢复执行
3) TASK_UNINTERRUPTIBLE: 不可中断的等待状态
处于该状态的进程正在等待某个事件(event)或某个资源,它肯定位于系统中的某个等待队列(wait_queue)中,处于不可中断等待态的进程是因为硬件环境不能满足而等待,例如等待特定的系统资源,它任何情况下都不能被打断,只能用特定的方式来唤醒它,例如唤醒函数wake_up()等
它们不能由外部信号唤醒,只能由内核亲自唤醒
4) TASK_ZOMBIE: 僵死
进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。
5) TASK_STOPPED: 暂停
此时的进程暂时停止运行来接受某种特殊处理。通常当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态
6) TASK_TRACED
从本质上来说,这属于TASK_STOPPED状态,用于从停止的进程中,将当前被调试的进程与常规的进程区分开来
7) TASK_DEAD
父进程wait系统调用发出后,当子进程退出时,父进程负责回收子进程的全部资源,子进程进入TASK_DEAD状态
8) TASK_SWAPPING: 换入/换出
*/
volatile long state;
/*
2. stack
进程内核栈,进程通过alloc_thread_info函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈
*/
void *stack;
/*
3. usage
进程描述符使用计数,被置为2时,表示进程描述符正在被使用而且其相应的进程处于活动状态
*/
atomic_t usage;
/*
4. flags
flags是进程当前的状态标志(注意和运行状态区分)
1) #define PF_ALIGNWARN 0x00000001: 显示内存地址未对齐警告
2) #define PF_PTRACED 0x00000010: 标识是否是否调用了ptrace
3) #define PF_TRACESYS 0x00000020: 跟踪系统调用
4) #define PF_FORKNOEXEC 0x00000040: 已经完成fork,但还没有调用exec
5) #define PF_SUPERPRIV 0x00000100: 使用超级用户(root)权限
6) #define PF_DUMPCORE 0x00000200: dumped core
7) #define PF_SIGNALED 0x00000400: 此进程由于其他进程发送相关信号而被杀死
8) #define PF_STARTING 0x00000002: 当前进程正在被创建
9) #define PF_EXITING 0x00000004: 当前进程正在关闭
10) #define PF_USEDFPU 0x00100000: Process used the FPU this quantum(SMP only)
#define PF_DTRACE 0x00200000: delayed trace (used on m68k)
*/
unsigned int flags;
/*
5. ptrace
ptrace系统调用,成员ptrace被设置为0时表示不需要被跟踪,它的可能取值如下:
linux-2.6.38.8/include/linux/ptrace.h
1) #define PT_PTRACED 0x00000001
2) #define PT_DTRACE 0x00000002: delayed trace (used on m68k, i386)
3) #define PT_TRACESYSGOOD 0x00000004
4) #define PT_PTRACE_CAP 0x00000008: ptracer can follow suid-exec
5) #define PT_TRACE_FORK 0x00000010
6) #define PT_TRACE_VFORK 0x00000020
7) #define PT_TRACE_CLONE 0x00000040
8) #define PT_TRACE_EXEC 0x00000080
9) #define PT_TRACE_VFORK_DONE 0x00000100
10) #define PT_TRACE_EXIT 0x00000200
*/
unsigned int ptrace;
unsigned long ptrace_message;
siginfo_t *last_siginfo;
/*
6. lock_depth
用于表示获取大内核锁的次数,如果进程未获得过锁,则置为-1
*/
int lock_depth;
/*
7. oncpu
在SMP上帮助实现无加锁的进程切换(unlocked context switches)
*/
#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
int oncpu;
#endif
#endif
/*
8. 进程调度
1) prio: 调度器考虑的优先级保存在prio,由于在某些情况下内核需要暂时提高进程的优先级,因此需要第三个成员来表示(除了static_prio、normal_prio之外),由于这些改变不是持久的,因此静态(static_prio)和普通(normal_prio)优先级不受影响
2) static_prio: 用于保存进程的"静态优先级",静态优先级是进程"启动"时分配的优先级,它可以用nice、sched_setscheduler系统调用修改,否则在进程运行期间会一直保持恒定
3) normal_prio: 表示基于进程的"静态优先级"和"调度策略"计算出的优先级,因此,即使普通进程和实时进程具有相同的静态优先级(static_prio),其普通优先级(normal_prio)也是不同的。进程分支时(fork),新创建的子进程会集成普通优先级
*/
int prio, static_prio, normal_prio;
/*
4) rt_priority: 表示实时进程的优先级,需要明白的是,"实时进程优先级"和"普通进程优先级"有两个独立的范畴,实时进程即使是最低优先级也高于普通进程,最低的实时优先级为0,最高的优先级为99,值越大,表明优先级越高
*/
unsigned int rt_priority;
/*
5) sched_class: 该进程所属的调度类,目前内核中有实现以下四种:
5.1) static const struct sched_class fair_sched_class;
5.2) static const struct sched_class rt_sched_class;
5.3) static const struct sched_class idle_sched_class;
5.4) static const struct sched_class stop_sched_class;
*/
const struct sched_class *sched_class;
/*
6) se: 用于普通进程的调用实体
调度器不限于调度进程,还可以处理更大的实体,这可以实现"组调度",可用的CPU时间可以首先在一般的进程组(例如所有进程可以按所有者分组)之间分配,接下来分配的时间在组内再次分配
这种一般性要求调度器不直接操作进程,而是处理"可调度实体",一个实体有sched_entity的一个实例标识
在最简单的情况下,调度在各个进程上执行,由于调度器设计为处理可调度的实体,在调度器看来各个进程也必须也像这样的实体,因此se在task_struct中内嵌了一个sched_entity实例,调度器可据此操作各个task_struct
*/
struct sched_entity se;
/*
7) rt: 用于实时进程的调用实体
*/
struct sched_rt_entity rt;
#ifdef CONFIG_PREEMPT_NOTIFIERS
/*
9. preempt_notifier
preempt_notifiers结构体链表
*/
struct hlist_head preempt_notifiers;
#endif
/*
10. fpu_counter
FPU使用计数
*/
unsigned char fpu_counter;
#ifdef CONFIG_BLK_DEV_IO_TRACE
/*
11. btrace_seq
blktrace是一个针对Linux内核中块设备I/O层的跟踪工具
*/
unsigned int btrace_seq;
#endif
/*
12. policy
policy表示进程的调度策略,目前主要有以下五种:
1) #define SCHED_NORMAL 0: 用于普通进程,它们通过完全公平调度器来处理
2) #define SCHED_FIFO 1: 先来先服务调度,由实时调度类处理
3) #define SCHED_RR 2: 时间片轮转调度,由实时调度类处理
4) #define SCHED_BATCH 3: 用于非交互、CPU使用密集的批处理进程,通过完全公平调度器来处理,调度决策对此类进程给与"冷处理",它们绝不会抢占CFS调度器处理的另一个进程,因此不会干扰交互式进程,如果不打算用nice降低进程的静态优先级,同时又不希望该进程影响系统的交互性,最适合用该调度策略
5) #define SCHED_IDLE 5: 可用于次要的进程,其相对权重总是最小的,也通过完全公平调度器来处理。要注意的是,SCHED_IDLE不负责调度空闲进程,空闲进程由内核提供单独的机制来处理
只有root用户能通过sched_setscheduler()系统调用来改变调度策略
*/
unsigned int policy;
/*
13. cpus_allowed
cpus_allowed是一个位域,在多处理器系统上使用,用于控制进程可以在哪里处理器上运行
*/
cpumask_t cpus_allowed;
/*
14. RCU同步原语
*/
#ifdef CONFIG_TREE_PREEMPT_RCU
int rcu_read_lock_nesting;
char rcu_read_unlock_special;
struct rcu_node *rcu_blocked_node;
struct list_head rcu_node_entry;
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
/*
15. sched_info
用于调度器统计进程的运行信息
*/
struct sched_info sched_info;
#endif
/*
16. tasks
通过list_head将当前进程的task_struct串联进内核的进程列表中,构建;linux进程链表
*/
struct list_head tasks;
/*
17. pushable_tasks
limit pushing to one attempt
*/
struct plist_node pushable_tasks;
/*
18. 进程地址空间
1) mm: 指向进程所拥有的内存描述符
2) active_mm: active_mm指向进程运行时所使用的内存描述符
对于普通进程而言,这两个指针变量的值相同。但是,内核线程不拥有任何内存描述符,所以它们的mm成员总是为NULL。当内核线程得以运行时,它的active_mm成员被初始化为前一个运行进程的active_mm值
*/
struct mm_struct *mm, *active_mm;
/*
19. exit_state
进程退出状态码
*/
int exit_state;
/*
20. 判断标志
1) exit_code
exit_code用于设置进程的终止代号,这个值要么是_exit()或exit_group()系统调用参数(正常终止),要么是由内核提供的一个错误代号(异常终止)
2) exit_signal
exit_signal被置为-1时表示是某个线程组中的一员。只有当线程组的最后一个成员终止时,才会产生一个信号,以通知线程组的领头进程的父进程
*/
int exit_code, exit_signal;
/*
3) pdeath_signal
pdeath_signal用于判断父进程终止时发送信号
*/
int pdeath_signal;
/*
4) personality用于处理不同的ABI,它的可能取值如下:
enum
{
PER_LINUX = 0x0000,
PER_LINUX_32BIT = 0x0000 | ADDR_LIMIT_32BIT,
PER_LINUX_FDPIC = 0x0000 | FDPIC_FUNCPTRS,
PER_SVR4 = 0x0001 | STICKY_TIMEOUTS | MMAP_PAGE_ZERO,
PER_SVR3 = 0x0002 | STICKY_TIMEOUTS | SHORT_INODE,
PER_SCOSVR3 = 0x0003 | STICKY_TIMEOUTS |
WHOLE_SECONDS | SHORT_INODE,
PER_OSR5 = 0x0003 | STICKY_TIMEOUTS | WHOLE_SECONDS,
PER_WYSEV386 = 0x0004 | STICKY_TIMEOUTS | SHORT_INODE,
PER_ISCR4 = 0x0005 | STICKY_TIMEOUTS,
PER_BSD = 0x0006,
PER_SUNOS = 0x0006 | STICKY_TIMEOUTS,
PER_XENIX = 0x0007 | STICKY_TIMEOUTS | SHORT_INODE,
PER_LINUX32 = 0x0008,
PER_LINUX32_3GB = 0x0008 | ADDR_LIMIT_3GB,
PER_IRIX32 = 0x0009 | STICKY_TIMEOUTS,
PER_IRIXN32 = 0x000a | STICKY_TIMEOUTS,
PER_IRIX64 = 0x000b | STICKY_TIMEOUTS,
PER_RISCOS = 0x000c,
PER_SOLARIS = 0x000d | STICKY_TIMEOUTS,
PER_UW7 = 0x000e | STICKY_TIMEOUTS | MMAP_PAGE_ZERO,
PER_OSF4 = 0x000f,
PER_HPUX = 0x0010,
PER_MASK = 0x00ff,
};
*/
unsigned int personality;
/*
5) did_exec
did_exec用于记录进程代码是否被execve()函数所执行
*/
unsigned did_exec:1;
/*
6) in_execve
in_execve用于通知LSM是否被do_execve()函数所调用
*/
unsigned in_execve:1;
/*
7) in_iowait
in_iowait用于判断是否进行iowait计数
*/
unsigned in_iowait:1;
/*
8) sched_reset_on_fork
sched_reset_on_fork用于判断是否恢复默认的优先级或调度策略
*/
unsigned sched_reset_on_fork:1;
/*
21. 进程标识符(PID)
在CONFIG_BASE_SMALL配置为0的情况下,PID的取值范围是0到32767,即系统中的进程数最大为32768个
#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)
在Linux系统中,一个线程组中的所有线程使用和该线程组的领头线程(该组中的第一个轻量级进程)相同的PID,并被存放在tgid成员中。只有线程组的领头线程的pid成员才会被设置为与tgid相同的值。注意,getpid()系统调用
返回的是当前进程的tgid值而不是pid值。
*/
pid_t pid;
pid_t tgid;
#ifdef CONFIG_CC_STACKPROTECTOR
/*
22. stack_canary
防止内核堆栈溢出,在GCC编译内核时,需要加上-fstack-protector选项
*/
unsigned long stack_canary;
#endif
/*
23. 表示进程亲属关系的成员
1) real_parent: 指向其父进程,如果创建它的父进程不再存在,则指向PID为1的init进程
2) parent: 指向其父进程,当它终止时,必须向它的父进程发送信号。它的值通常与real_parent相同
*/
struct task_struct *real_parent;
struct task_struct *parent;
/*
3) children: 表示链表的头部,链表中的所有元素都是它的子进程(子进程链表)
4) sibling: 用于把当前进程插入到兄弟链表中(连接到父进程的子进程链表(兄弟链表))
5) group_leader: 指向其所在进程组的领头进程
*/
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;
struct list_head ptraced;
struct list_head ptrace_entry;
struct bts_context *bts;
/*
24. pids
PID散列表和链表
*/
struct pid_link pids[PIDTYPE_MAX];
/*
25. thread_group
线程组中所有进程的链表
*/
struct list_head thread_group;
/*
26. do_fork函数
1) vfork_done
在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特殊地址
2) set_child_tid、clear_child_tid
如果copy_process函数的clone_flags参数的值被置为CLONE_CHILD_SETTID或CLONE_CHILD_CLEARTID,则会把child_tidptr参数的值分别复制到set_child_tid和clear_child_tid成员。这些标志说明必须改变子
进程用户态地址空间的child_tidptr所指向的变量的值。
*/
struct completion *vfork_done;
int __user *set_child_tid;
int __user *clear_child_tid;
这里我们给出了task_struct的部分的结构,可以这么说,task_struct中存放了一个进程的几乎的所有的信息
task_struct如何管理进程
当我们的进程进入的操作的之前,我们的操作系统就会上产生一个独属于本进程的task_struct结构体,他在进程创建之前就完成了创建,他在进程销毁之后才销毁.那么根据我们先描述再组织的理论,
我们这里总结一下什么是进程?
总结:
进程 = task_struct + 代码和数据
个体 = 户口本 + 加上我们自己
Linux中tast_struct中保存的信息的概念
进程的状态变量
- R状态:运行时状态
- S状态: -- 阻塞等待状态 -- 可中断睡眠,浅睡眠 -- 可中断睡眠
- D状态 磁盘 -- 阻塞等待状态 -- 不可中断睡眠 -- 深度睡眠如果进程处于D状态,进程不能够被操作系统直接kill掉,这样保证了进程和磁盘io一定不会被操作系统失误的kill掉所以平时当我们查到D状态,要么就是系统快要挂了,要么就是高io的情况
- T状态
1.进程做了非法的操作,但是不致命
- t状态
僵尸进程
僵尸进程:进程处于Z状态是一种比较特殊的状态,这种状态我们称之为僵死状态也称为僵尸状态,什么意思呢,就是说我们的进程退出后,没有被父进程进行回收的阶段我们将他称作僵尸状态。
僵尸状态必须满足两个条件。
1. 子进程退出后,没有被父进程回收
2. 父进程并没有退出
这种情况下,就会出现僵尸的状态。用一种比较通俗的理解类似于malloc没有free,new没有delete。
僵尸进程的危害:
一个父进程创建了很多的子进程,创建子进程是需要Linux内核随之创建对应的数据结构的占用对应的内存,如果我们创建了很多进程但是并不对进程进行回收,这样的本质其实也是一种内存泄露。
进程中状态的查找命令
ps ajx | grep 'taskname'
ps ajx命令帮助我们去/etc/proc中找到我们的进程的信息,通过管道grep 'taskname'找到对应的进程的运行的状态这里的S+就是我们正在执行的进程的状态,S+的意思就是这个进程是在前台进行
进程终止命令
kill命令,我们可以通过kill命令杀进程,kill -l表示杀进程的选项,我们可以重新登录一个终端,在另一个终端上面进行杀进程或者查看进程状态的情况
进程的调度
我们知道进程加载进入内存中后,不同的进程之间执行的工作千差万别,那么我们的操作系统应当将这些复杂的关系进行统一的管理和调度呢?
我们知道,当我们的进程加载进入操作系统之后,我们的操作系统就会为他创建一个task_struct,也就是进程出生后,我们的操作系统给他创建一个户口.然后我们操作系统其实可以通过对task_struct进行管理然后间接的对进程进行管理.task_struct作为一种内核数据结构,我们可以通过数据结构的管理方式对其进行管理. 举个例子我们可以在设计一种链表的结构,链表中元素的类型是struct task_struct*的指针,这样我们就实现了通过链表对task_struct进行间接管理.
那么,在Linux操作系统中我们是怎样对进程进行管理和调度的呢
进程的优先级
为什么会有进程的优先级?首先,Linux操作系统是一款实时操作系统,实时操作系统就是对于每个进程来说,操作系统默认他们是等价的,Linux总体上是实时操作系统,但是他同时也是支持用户修改其中进程的优先级.
首先我们需要讲解的是Linux中进程调度队列的数据结构原理
每个cpu都分配了两个运行队列,两个队列都是tast_struct* queue[140],可以存储140个tast_struct(pcb)结构体,通过这两个队列分别存储了一个int active的值,bitmap[5],我们对这些结构分别解释.首先queue是一个开散列hash数组,我们调节进程的优先级的时候,我们对应优先级的hash映射映射到数组中,优先级越高映射越前面,也就是越先执行.bitmap位图,虽然进程运行队列有140个位置,但是实际我们能够操作的只有后40个,所以bitmap[5]这个位图刚好可以实现对40位的管理.最后说active值的意义,每个cpu中都有两个运行时队列,一个是active,另一个是not active,
那么你可能会有疑问?就是说如果我创建一个新的进程进入active还是not active,答案当然就是not active了,因为就是说正在调用的进程队列我们不应该直接就去修改他,而是应该等待他调度完成之后,调度我们的not active的队列。
创建子进程
在Linux中,内核为我们提供了创建子进程的系统级别的接口
pid_t fork();
fork()函数的返回值有两个,对于父进程来说,pid是子进程的pid,对于子进程来说,返回值就是0,表示的是创建子进程成功,但是如果说返回值只有一个并且为-1,表示的是就是进程创建失败。在Linux中许多的错误都是通过返回-1进行提示。
下面这里就给出一个简单的创建子进程,通过子进程完成任务的示例代码。
总结:
这里只是从概念的层面介绍了进程或者说Linux下面什么是进程并且我们可以通过什么样的方式创建一个进程。