【Linux笔记】Linux进程概念与进程状态
- 一、什么是进程
- 1.1、进程的概念
- 1.2、进程的描述
- 二、关于进程的一些基本操作
- 2.1、查看进程
- 2.2、杀进程
- 2.3、获取进程id
- 2.4、创建进程
- 三、进程状态
- 3.1、普适操作系统中的进程状态
- 3.2、具体到Linux操作系统中的进程状态
- 四、僵尸进程和孤儿进程
- 4.1、僵尸进程
- 4.2、孤儿进程
- 五、进程的优先级
- 5.1、查看进程的优先级
- 5.2、怎样修改进程的优先级
一、什么是进程
1.1、进程的概念
进程的概念:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
通常在一些计算机或操作系统的书籍上也会这样告诉我们:进程是被加载到内存中的程序,或者运行起来的程序就叫做进程。
1.2、进程的描述
而进程再被加载到内存中后,操作系统就一定要对其进行管理,而在操作系统看来对程序的管理本质上就是对程序数据的管理,所以操作系统就会从这些程序中抽象出一些共有属性来构建出一个结构体,然后为每一个进程都创建一个这样的结构体对象。
然后操作系统对进程的管理就转化成了对这些结构体对象的管理了,在操作系统中,这个用于描述和组织进程的东西被称为 进程控制块——PCB。
而进程控制块在不同的操作系统中的结构和名称也可能不一样,具体到Linux操作系统中的进程控制块是task_struct,它是Linux内核中的一种数据结构,其包含的内如大致如下:
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
task_struct的源码肯定是非常复杂的,但没关系我们可以先了解一下。
我们可以来看看对应的task_struct的源码:
struct task_struct {
/*
* 进程状态。
*/
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
/*
* 进程的基本信息。
*/
struct thread_info *thread_info;
atomic_t usage;
unsigned long flags; /* per process flags, defined below */
unsigned long ptrace;
int lock_depth; /* Lock depth */
/*
* 进行的动态优先权和静态优先权
*/
int prio, static_prio;
/*
* 进程所在运行队列。每个优先级对应一个运行队列。
*/
struct list_head run_list;
/*
* 指向当前运行队列的prio_array_t
*/
prio_array_t *array;
/*
* 进程的平均睡眠时间
*/
unsigned long sleep_avg;
/*
* timestamp:进程最近插入运行队列的时间。或涉及本进程的最近一次进程切换的时间
* last_ran:最近一次替换本进程的进程切换时间。
*/
unsigned long long timestamp, last_ran;
/*
* 进程被唤醒时所使用的代码。
* 0:进程处于TASK_RUNNING状态。
* 1:进程处于TASK_INTERRUPTIBLE或者TASK_STOPPED状态,而且正在被系统调用服务例程或内核线程唤醒。
* 2:进程处于TASK_INTERRUPTIBLE或者TASK_STOPPED状态,而且正在被ISR或者可延迟函数唤醒。
* -1:表示从UNINTERRUPTIBLE状态被唤醒
*/
int activated;
/*
* 进程的调度类型:sched_normal,sched_rr或者sched_fifo
*/
unsigned long policy;
/*
* 能执行进程的CPU的位掩码
*/
cpumask_t cpus_allowed;
/*
* time_slice:在进程的时间片中,还剩余的时钟节拍数。
* first_time_slice:如果进程肯定不会用完其时间片,就把该标志设置为1。
*
* Dagger-axe注:目的为加速子进程的返回,防止内存中进程过多。
*/
unsigned int time_slice, first_time_slice;
#ifdef CONFIG_SCHEDSTATS
struct sched_info sched_info;
#endif
/*
* 通过此链表把所有进程链接到一个双向链表中。
*/
struct list_head tasks;
/*
* ptrace_list/ptrace_children forms the list of my children
* that were stolen by a ptracer.
*/
/*
* 链表的头。该链表包含所有被debugger程序跟踪的P的子进程。
*/
struct list_head ptrace_children;
/*
* 指向所跟踪进程的实际父进程链表的前一个下一个元素。
*/
struct list_head ptrace_list;
/*
* mm:指向内存区描述符的指针
*/
struct mm_struct *mm, *active_mm;
/* task state */
struct linux_binfmt *binfmt;
long exit_state;
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
unsigned long personality;
/*
* 进程发出execve系统调用的次数。
*/
unsigned did_exec:1;
/*
* 进程PID
*/
pid_t pid;
/*
* 线程组领头线程的PID。
*/
pid_t tgid;
/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->parent->pid)
*/
/*
* 指向创建进程的进程的描述符。
* 如果进程的父进程不再存在,就指向进程1的描述符。
* 因此,如果用户运行一个后台进程而且退出了shell,后台进程就会成为init的子进程。
*/
struct task_struct *real_parent; /* real parent process (when being debugged) */
/*
* 指向进程的当前父进程。这种进程的子进程终止时,必须向父进程发信号。
* 它的值通常与real_parent一致。
* 但偶尔也可以不同。例如:当另一个进程发出监控进程的ptrace系统调用请求时。
*/
struct task_struct *parent; /* parent process */
/*
* children/sibling forms the list of my children plus the
* tasks I'm ptracing.
*/
/*
* 链表头部。链表指向的所有元素都是进程创建的子进程。
*/
struct list_head children; /* list of my children */
/*
* 指向兄弟进程链表的下一个元素或前一个元素的指针。
*/
struct list_head sibling; /* linkage in my parent's children list */
/*
* P所在进程组的领头进程的描述符指针。
*/
struct task_struct *group_leader; /* threadgroup leader */
/* PID/PID hash table linkage. */
/*
* PID散列表。通过这四个表,可以方便的查找同一线程组的其他线程,同一会话的其他进程等等。
*/
struct pid pids[PIDTYPE_MAX];
struct completion *vfork_done; /* for vfork() */
/*
* 子进程在用户态的地址。这些用户态地址的值将被设置或者清除。
* 在do_fork时记录这些地址,稍后再设置或者清除它们的值。
*/
int __user *set_child_tid; /* CLONE_CHILD_SETTID */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
/*
* 进程的实时优先级。
*/
unsigned long rt_priority;
/*
* 以下三对值用于用户态的定时器。当定时器到期时,会向用户态进程发送信号。
* 每一对值分别存放了两个信号之间以节拍为单位的间隔,及定时器的当前值。
*/
unsigned long it_real_value, it_real_incr;
cputime_t it_virt_value, it_virt_incr;
cputime_t it_prof_value, it_prof_incr;
/*
* 每个进程的动态定时器。用于实现ITIMER_REAL类型的间隔定时器。
* 由settimer系统调用初始化。
*/
struct timer_list real_timer;
/*
* 进程在用户态和内核态下经过的节拍数
*/
cputime_t utime, stime;
unsigned long nvcsw, nivcsw; /* context switch counts */
struct timespec start_time;
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
unsigned long min_flt, maj_flt;
/* process credentials */
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
struct group_info *group_info;
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
unsigned keep_capabilities:1;
struct user_struct *user;
#ifdef CONFIG_KEYS
struct key *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process (CLONE_THREAD) */
struct key *thread_keyring; /* keyring private to this thread */
#endif
int oomkilladj; /* OOM kill score adjustment (bit shift). */
char comm[TASK_COMM_LEN];
/* file system info */
/*
* 文件系统在查找路径时使用,避免符号链接查找深度过深,导致死循环。
* link_count是__do_follow_link递归调用的层次。
* total_link_count调用__do_follow_link的总次数。
*/
int link_count, total_link_count;
/* ipc stuff */
struct sysv_sem sysvsem;
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
/*
* 与文件系统相关的信息。如当前目录。
*/
struct fs_struct *fs;
/* open file information */
/*
* 指向文件描述符的指针
*/
struct files_struct *files;
/* namespace */
struct namespace *namespace;
/* signal handlers */
/*
* 指向进程的信号描述符的指针
*/
struct signal_struct *signal;
/*
* 指向进程的信号处理程序描述符的指针
*/
struct sighand_struct *sighand;
/*
* blocked:被阻塞的信号的掩码
* real_blocked:被阻塞信号的临时掩码(由rt_sigtimedwait系统调用使用)
*/
sigset_t blocked, real_blocked;
/*
* 存放私有挂起信号的数据结构
*/
struct sigpending pending;
/*
* 信号处理程序备用堆栈的地址
*/
unsigned long sas_ss_sp;
/*
* 信号处理程序备用堆栈的大小
*/
size_t sas_ss_size;
/*
* 指向一个函数的指针,设备驱动程序使用这个函数阻塞进程的某些信号
*/
int (*notifier)(void *priv);
/*
* 指向notifier函数可能使用的数据
*/
void *notifier_data;
sigset_t *notifier_mask;
void *security;
struct audit_context *audit_context;
/* Thread group tracking */
u32 parent_exec_id;
u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty, keyrings */
spinlock_t alloc_lock;
/* Protection of proc_dentry: nesting proc_lock, dcache_lock, write_lock_irq(&tasklist_lock); */
spinlock_t proc_lock;
/* context-switch lock */
spinlock_t switch_lock;
/* journalling filesystem info */
/*
* 当前活动日志操作处理的地址。
* 正在使用的原子操作对象。
*/
void *journal_info;
/* VM state */
struct reclaim_state *reclaim_state;
struct dentry *proc_dentry;
struct backing_dev_info *backing_dev_info;
struct io_context *io_context;
unsigned long ptrace_message;
siginfo_t *last_siginfo; /* For ptrace use. */
/*
* current io wait handle: wait queue entry to use for io waits
* If this thread is processing aio, this points at the waitqueue
* inside the currently handled kiocb. It may be NULL (i.e. default
* to a stack based synchronous wait) if its doing sync IO.
*/
wait_queue_t *io_wait;
/* i/o counters(bytes read/written, #syscalls */
u64 rchar, wchar, syscr, syscw;
#if defined(CONFIG_BSD_PROCESS_ACCT)
u64 acct_rss_mem1; /* accumulated rss usage */
u64 acct_vm_mem1; /* accumulated virtual memory usage */
clock_t acct_stimexpd; /* clock_t-converted stime since last update */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *mempolicy;
short il_next;
#endif
};
二、关于进程的一些基本操作
2.1、查看进程
查看进程有很多种方式,我们今天主要看两种。
第一种:使用ps axj配合grep和管道来查看指定进程。
比如我们现在有一段这样的C程序:
#include <stdio.h>
#include <unistd.h>
int main() {
while (1) {
printf("我是一个进程,我正在努力的跑!!\n");
sleep(1);
}
return 0;
}
我们知道当它在内存中运行的时候,就变成了一个进程,所以我们先让它一直运行着:
然后我们可以再复制一个SSH渠道去监视它:
第二种方法:在 “/proc” 系统文件夹中查看所有进程:
但是这里所显示的进程都是以进程的id命名的,所以我们可以先查出进程的id再去寻找:
其中加上head -1这个指令表示的是显示第一行的信息,即头部信息。
2.2、杀进程
向上述一样,我们启动了一个死循环的进程,他一直在命令行中打印内容。那我们怎么技术它呢?
第一个方法就是在命令行中按下ctrl + c组合键:
还有一种方法就是在另一个终端中,使用kill命令指定-9选项再加上进程id来结束:
2.3、获取进程id
我们刚才一直提到进程的id,那它有什么用呢?我们是否可以在代码中获取到对应进程的id呢?
关于进程的id(pid)有什么用我们下面再介绍,我们先来介绍怎样在进程中获取对应进程的pid。
我们可以使用操作系统为我们提供的系统调用 getpid()和getppid() 来获取进程及其父进程的id:
关于getpidd()和getppid()的返回值,它其实是一个,名为pid_t的类型。我们平时使用仅需要将他看成是int类型即可,打印时也可以使用%d.
2.4、创建进程
我们可以使用系统调用接口fork()创建子进程:
大家可能看到这样的结果会感到疑惑,为什么子进程的代码会执行了两次,而且看pid会发现有一次还是父进程执行的。
其实fork这样使用是不对的,想要正确的使用我们得先看看fork的使用手册:
这个接口“有两个返回值”,如果创建成功那子进程的pid将返回给父进程,0将返回给子进程,如果创建失败则返回-1。
至于为什么一个函数会有两个返回值,这是一个非常复杂的问题,这需要我们以后再洗洗探讨,我们现在先知道有这么个现象即可。
而fork正确的使用方法是通过fork的返回值对父子进程进行“分流”,即父子进程执行不同的代码。
很简单,我们可以通过判断getpid的返回值来完成:
从结果可以看出,进程不仅仅行了分流,而且默认是父进程先执行。
三、进程状态
我们知道操作系统对进程的管理本质上就是对进程控制块PCB的管理,而进程状态其实就是PCB内部的一个整型变量,根据不同的整型值来标识进程不同的状态。
3.1、普适操作系统中的进程状态
在很多将操作系统的书上都是将进程的状态分为如下几种:运行、挂起、阻塞、新建、就绪、等待、挂机、死亡。
而各种教材上面的各种状态的对应的名称也可能不一样,但是他们想要表达的意思是一样的。
其实只要是操作系统,他们的内核都大差不差,只是有些细微的地方不一样而已。
3.2、具体到Linux操作系统中的进程状态
而具体到Linux操作系统,进程的主要状态有以下六种:
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
D磁盘休眠状态(Disk sleep): 有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead): 这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
僵死状态(Zombies): 是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
而这些状态,在内核中其实是对应着一个任务状态数组,这实际上是一个字符数组,我们为此可以猜测出,进程的状态其实也是根据这个字符数组中对应的字符来标识的:
/*
* 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 */
};
运行状态:
运行状态其实很好理解,只要一个进程正在运行(即进程的PCB位于CPU的运行队列中):
因为进程一直对a进行+1操作,所以它一直会处于运行状态。
注意: 如果你想进行测试,是一个进程一直处于运行状态,注意一定不使用printf()函数(比如死循环打印),因为这涉及到了io资源的申请,会让进程处于另一个状态:睡眠状态,这个下面会详细讲到。
睡眠状态
睡眠状态其实就是普适操作系统中的阻塞状态,因为进程需要等待某种资源:
但是有的朋友可能会不解了,这里看到的今晨不是一直在运行着吗,为什么就是睡眠状态呢?
这是因为我们cpu的速度是非常快的,我们人是根本感知不到的,printf需要等待显示器的资源,我们人虽然看到的是屏幕上飞速的打印着,但是在cpu看来就是它等待了很久才能打印一次,所以我们的这个进程其实大部分时间都在等待资源,所以也就是睡眠状态。
深度睡眠状态
这个状态就没办法给大家演示了,因为这是个非常危险的状态,只有在内存空间严重不足时才会出现。如果系统出现了深度睡眠状态,那这个系统也即将崩溃了。而我们想演示也是没有办法做到的,因为它只能通过操作系统来到达。
当操作系统出现空间严重不足时,操作系统会将某些进程直接杀掉来保证系统能够正常运行,而有些进程是非常重要的,如果直接杀掉的话就会造成巨大的损失,这时候操作系统就会讲这些非常重要的进程转换到深度睡眠状态,其做法就是将这个进程的数据写入磁盘,所以这个状态也称为磁盘休眠状态。
处于深度睡眠状态的进程既不能被用户杀掉也不能被操作系统杀掉,只能通过断电或者等待进程自己醒来
暂停状态
在进程中有很多的“信号”,使用这些信号也可以将我们的进程设置成指定的状态,我们可以使用kill -l指令来查看系统信号的详细列表:
我们今天主要看的是19这个信号,它的作用就是将一个进程设置成暂停状态:
如果我们想让这个进程恢复运行,可以使用-18选项:
死亡状态
死亡状态代表着一个进程已经完全结束,进程的PCB和代码数据全部被操作系统回收。
僵尸状态
我们的进程是为了帮助我们完成某种任务,而既然是完成任务,那进程在结束钱就应该返回任务执行的结果,供父进程或操作系统读取。
所以一个进程在退出的时候,并不能直接释放全部资源,对于进程的代码和数据,操作系统可以释放,因为该进程已经不会被执行了,但是该进程的PCB应该被保留,因为PCB中存放着该进程的各种状态代码,特别是退出状态码。
而僵尸状态就是进程在退出时等待父进程或操作系统来读取退出状态码,然后释放PCB的一种状态。
四、僵尸进程和孤儿进程
4.1、僵尸进程
上面我们提到一个进程在全部释放之前需要父进程或操作系统来读取进程的退出状态代码,而如果父进程一直读取子进程的退出状态码,那子进程的PCB将一直得不到释放,此时该进程就变成僵尸进程:
我们设计这样一个程序,让子进程只执行5次,然后让子进程退出,让父进程一直在执行:
然后我们在查看对应的子进程就会发现子进程已经变成了僵尸进程了:
这是因为虽然子进程退出了,但是父进程一直在执行,并不能读取子进程的退出码。
4.2、孤儿进程
孤儿进程是指父进程提前退出后,子进程被操作系统领养的一种情况,被操作系统领养的进程就被称为孤儿进程:
我们只需要将之前的代码改成让父进程先退出,然后子进程一直执行就可以模拟出孤儿进程了:
五、进程的优先级
进程的优先级与权限不同,权限是限制那些事情能做安歇事情不能做,而优先级则是规定那些事情先做那些事情后做。
因为CPU的资源是有限的,所以肯定不可能让所有进程都同时运行。所以规定哪个进程先运行哪个进程后运行也就变得至关重要了。所以操作系统就要为不同的进程设置不同的优先级。进程的优先级就是一个整型数字,这个数字越大则优先级越低,数字越小则优先级越高。
5.1、查看进程的优先级
我们可以使用ps -la指令来查看系统内的进程优先级:
其中这个PRI标题就是进程的优先级,它其实就是英文priority的缩写。而Linux中进程的优先级范围为60-99,Linux中默认的进程优先级为80。
5.2、怎样修改进程的优先级
Linux支持动态的优先级调整,单兵不允许我们直接的修改进程的优先级,我们只能通过修改进程PCB中的一个nice值来间接的修改优先级,而这里的NI就是进程对应的nice值:
进程的优先级的计算方式:
pri(新) = pri(老) + nice
因为nice的默认值为0,所以进程的默认优先级也就为80了,所以我们可以通过修改nice值来达到修改优先级的目的,修改nice值的其中一个方法的步骤为:
输入top --> 输入r --> 输入进程id --> 输入NI值
而我们若想把优先级调高就需要root权限:
而既然Linux已经规定了进程的优先级范围是60-99那也就说明了我们并不能随意地修改进程的优先级,间接的也就不能随意的修改nice值。
通过计算我们得出,规定了进程的优先级范围是60-99那nice值的范围也就是[-20 - 19]:
如果我们修改的范围超过了这个区间,那系统就会把nice只修改为边界情况:
注:公式 “pri(新) = pri(老) + nice” 中的老优先级每一次都是默认优先级80。