基本概念
进程是程序的一个执行实例,是一个正在执行的程序。
进程能分配处理器并由处理器执行的实体。
如果从内核的角度看的话:进程是分配系统资源的单位。当一个程序被加载到内存之后并为他分配一个PCB(进程控制块),就称之为进程。在Linux中PCB就是一个名字叫做task_struct的结构体,我们叫他“进程描述符”。它里面有进程执行的所有信息,所以CPU对task_struct进行管理就相当于在对进程进行管理。
- 程序:程序是静止的,是磁盘上的一个文件。
- 课本知识:程序的一个执行实例,正在执行的程序等(动态的)。
- 内核观点:担当分配系统资源(CPU时间、内存)的实体。
- 程序一旦被操作系统加载到内存中,那么程序就变成了进程。
- 进程信息放在一个叫进程控制块(PCB)的数据结构中,可以理解为进程属性的集合。
- Linux中描述进程的结构体叫做task_struct。
- task_struct是Linux内核的一种数据结构,会被装载到RAM(内存)里并且包含着进程的信息
在linux中,每一个进程都有一个进程描述符,这个“进程描述符”是一个结构体名字叫做task_struct,在task_struct里面保存了许多关于进程控制的信息。
task_struct是Linux内核的一种数据结构,它会被装载到RAM里并包含进程的信息。每个进程都把它的信息放在task_struct这个数据结构里面,而task_struct包含以下内容:
- 标识符:跟这个进程相关的唯一标识符,用来区分其他进程。
- 状态:任务状态,退出代码,退出信号等。。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享内存块的指针。
- 上下文数据:程序执行时处理器的寄存器中的数据。
- I/O状态信息:包括显示的I/O请求、分配给进程的I/O设备和被进程使用的文件列表。
- 记帐信息:可能包括处理器时间总和、使用的时钟数总和、时间限制、记账号等。
因此,可以说进程是由程序代码和相关数据还有进程控制块组成。对于一个单处理器计算机,在任何时间最多只有一个进程在运行,正在运行的这个进程状态为运行态。
task_struct定义:
struct task_struct {
volatile long state; //说明了该进程是否可以执行,还是可中断等信息
unsigned long flags; //Flage 是进程号,在调用fork()时给出
int sigpending; //进程上是否有待处理的信号
mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同
//0-0xBFFFFFFF for user-thead
//0-0xFFFFFFFF for kernel-thread
//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
volatile long need_resched;
int lock_depth; //锁深度
long nice; //进程的基本时间片
//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER
unsigned long policy;
struct mm_struct *mm; //进程内存管理信息
int processor;
//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新
unsigned long cpus_runnable, cpus_allowed;
struct list_head run_list; //指向运行队列的指针
unsigned long sleep_time; //进程的睡眠时间
//用于将系统中所有的进程连成一个双向循环链表, 其根是init_task
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_head local_pages; //指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal; //父进程终止时向子进程发送的信号
unsigned long personality;
//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
int did_exec:1;
pid_t pid; //进程标识符,用来代表一个进程
pid_t pgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_t session; //进程的会话标识
pid_t tgid;
int leader; //表示进程是否为会话主管
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; //线程链表
struct task_struct *pidhash_next; //用于将进程链入HASH表
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; //供wait4()使用
struct completion *vfork_done; //供vfork() 使用
unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
//it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value
//设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据
//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。
//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送
//信号SIGPROF,并根据it_prof_incr重置时间.
//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种
//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据
//it_virt_incr重置初值。
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_list real_timer; //指向实时定时器的指针
struct tms times; //记录进程消耗的时间
unsigned long start_time; //进程创建的时间
//记录进程在每个CPU上所消耗的用户态时间和核心态时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
//内存缺页和交换信息:
//min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换
//设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。
//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid
//euid,egid为有效uid,gid
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件
//系统的访问权限时使用他们。
//suid,sgid为备份uid,gid
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; //记录进程在多少个用户组中
gid_t groups[NGROUPS]; //记录进程所在的组
//进程的权能,分别是有效位集合,继承位集合,允许位集合
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
unsigned short used_math; //是否使用FPU
char comm[16]; //进程正在运行的可执行文件名
//文件系统信息
int link_count, total_link_count;
//NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空
struct tty_struct *tty;
unsigned int locks;
//进程间通信信息
struct sem_undo *semundo; //进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
//进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct thread_struct thread;
//文件系统信息
struct fs_struct *fs;
//打开文件信息
struct files_struct *files;
//信号处理函数
spinlock_t sigmask_lock;
struct signal_struct *sig; //信号处理函数
sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位
struct sigpending pending; //进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock;
void *journal_info;
};
进程的创建和终止
- 进程的创建:
当一个新进程添加到那些正在被管理的进程集合中去时,操作系统需要建立用于管理该进程的数据结构,并在内存中给它分配地址空间,这些行为构成了一个新进程的创建过程。通常会有以下4个事件导致创建一个新进程:
- 进程的终止:
任何一个计算机都必须为进程提供表示其完成的方法,批量处理作业中应该包含一个Halt指令或用于终止操作系统显示服务调用来终止。在前一种情况下,Halt指令将产生一个中断,警告操作系统一个进程已经完成。对交互式应用程序,用户的行为将指出何时进程完成。在有些操作系统中,进程可以被创建它的进程终止,或当父进程终止而终止。以下是进程终止的几种情况:
查看进程
PID:pid_t getpid(void);得到进程的PID
PID是程序被操作系统加载到内存成为进程后动态分配的资源。
每次程序执行的时候,操作系统都会重新加载,PID在每次加载的时候都是不同的。PPID:pid_t getppid(void);得到进程的PPID
PPID是程序的父进程号。一个进程创建的另一个新进程称为子进程。相反地,创建子进程的进程称为父进程。对于一个普通的用户进程,它的父进程就是执行它的那个Shell,对于Linux而言,Shell就是bash。
所有进程的祖先为init进程。
创建一个进程:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int mian(){
while(1){
sleep(1);
}
return 0;
}
通过系统调用获取进程标识符
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int mian(){
pid_t pid = getpid();
printf("pid:%d\n",pid);
pid_t ppid = getppid();
printf("ppid:%d\n",ppid);
while(1){
sleep(1);
}
return 0;
}
ps axj | grep ./a.out
通过系统调用创建进程
fork特点:
一次调用有两个返回值,父进程返回子进程PID,子进程返回0。
父进程和子进程都从fork执行结束后的位置继续执行。
子进程以父进程为模板(PCB,数据和代码)。写时拷贝。
父子进程执行的先后顺序不确定,取决于操作系统调度器。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int mian(){
int ret = fork();
printf("pid: %d, ppid: %d, ret: %d\n", getpid(), getppid(), ret);
while(1){
sleep(1);
}
return 0;
}
使用if进行分流:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int mian(){
int ret = fork();
if(ret < 0){
perror("fork");
return 1;
}
else if(ret == 0){
printf("I am child: pid: %d, ppid: %d, ret: %d\n", getpid(), getppid(), ret);
}
else{
printf("I am father: pid: %d, ppid: %d, ret: %d\n", getpid(), getppid(), ret);
}
while(1){
sleep(1);
}
return 0;
}
进程状态
R(就绪):表明进程要么在运行中,要么在运行队列中
S(挂起):进程在等待事件完成(可中断睡眠)
D(深度睡眠):进程通常会等待I/O结束(不可中断睡眠)
T(暂停):可以通过发送SIGSTOP信号停止进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
t(跟踪)
X(死亡):只是一个返回状态。
z(僵尸状态):它的父进程尚未调用wait函数
停止9037号进程:
恢复:
僵尸进程
当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。
僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
int ret = fork();
if(ret < 0){
perror("fork");
return 1;
}
else if(ret > 0){
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}
else{
printf("child[%d] is begin z...\n",getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
僵尸进程会占用系统资源,如果很多,则会严重影响服务器的性能
若一直不回收,有可能造成内存泄漏
孤儿进程
- 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int mian(){
int ret = fork();
if(ret < 0){
perror("fork");
return 1;
}
else if(ret == 0){
printf("I am child: pid: %d, ppid: %d, ret: %d\n", getpid(), getppid(), ret);
sleep(10);
}
else{
printf("I am father: pid: %d, ppid: %d, ret: %d\n", getpid(), getppid(), ret);
sleep(3);
exit(0);
}
return 0;
}