Linux系统——进程概念(上)

📅 2026/8/16 22:22:28
Linux系统——进程概念(上)
‍♂️个人主页进击的荆棘作者其它专栏《数据结构与算法》《算法》《C起始之路》《Linux》目录1.冯诺依曼体系结构2.操作系统Operator System3.进程4.命令行参数和环境变量5.程序地址空间1.冯诺依曼体系结构常见的计算机如笔记本。不常见的计算器如服务器大部分都遵守冯诺依曼体系。截止目前我们所认识的计算机都是由一个个的硬件组件组成●输入单元键盘鼠标扫描仪写板等●中央处理器CPU运算器和控制器等●输出单元显示器打印机等关于冯诺依曼●这里的存储器指的是内存●不考虑缓存情况这里的CPU能且只能对内存进行读写不能访问外设输入或输出设备数据层面●外设输入或输设备要输入或输出数据也只能写入内存或从内存中读取●即所有设备都只能直接和内存打交道体系结构的效率由“拷贝”效率决定注意对冯诺依曼的理解不能停留在概念上要深入到对软件数据流理解上如qq上和朋友聊天数据的流动过程2.操作系统Operator System2.1概念任何计算机系统都包含一个基本的程序集合称为操作系统OS。笼统的理解操作系统包括●内核进程管理内存管理文件管理驱动管理●其它程序例如函数库shell程序等等2.2设计OS的目的●对上与硬件交互管理所有的软硬件资源●对下为用户程序应用程序提供一个良好的执行环境2.3核心功能●在整个计算机软硬件架构中操作系统的定位是一款纯正的“搞管理”的软件2.4如何理解“管理”●管理的例子-学生辅导员校长●描述被管理的对象●组织被管理对象总结计算机管理硬件1.描述起来用struct结构体2.组织起来用链表或其它高效的数据结构2.5系统调用和库函数概念●在开发角度操作系统对外会表现为一个整体到那时会暴露自己的部分接口供上层开发使用这部份由操作系统提供的接口叫做系统调用●系统调用在使用上功能比较基础对用户的要求相对也比较高所以开发者可以对部分系统调用进行适度封装从而形成库有了库就很有利与更上层用户或开发者进行二次开发操作系统是怎样进行进程管理的先把进程描述起来再把进程组织起来3.进程3.1基本概念与基本操作●课本概念程序的一个执行实例正在执行的程序等●内核观点担当分配系统资源CPU时间内存的实体●当前进程内核数据结构task_struct自己的程序代码和数据3.1.1描述进程-PCB基本概念●进程信息被放到一个叫做进程控制块的数据结构中可以理解为进程属性的集合●课本上称之为PCBprocess control blockLinux操作系统下的PCB是task_structtask_struct PCB的一种●在Linux中描述进程的结构体叫做task_struct●task_struct是Linux内核的一种数据结构类型它会被装载到RAM内存里并且包含着进程的信息3.1.2task_struct内容分类●标识符描述本进程的唯一标识符用来区别其它进程●状态任务状态退出代码退出信号等●优先级相对于其它进程的优先级●程序计数器程序中即将被执行的下一条指令的地址●内存指针包括程序代码和进程相关数据的指针还有和其它进程共享的内存块的指针●上下文数据进程执行时处理器的寄存器中的数据[休学例子要加图CPU寄存器]●I/O状态信息包括显示的I/O请求分配给进程的I/O设备和被进程使用的文件列表●记账信息可能包括处理器时间总和使用的时钟数总和时间限制记帐号等●其它信息组织进程可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct双链表的形式存在于内核里3.1.3查看进程1.进程的信息可以通过/proc系统文件夹查看如要获取PID为1的进程信息需要查看/proc/1这个文件夹2.大多数进程信息同样可以使用top和ps这些用户级工具来获取#include stdio.h #include sys/types.h #include unistd.h int main(){ while(1){ sleep(1); } return 0; }3.1.4通过系统调用获取进程标识符●进程idPID●父进程idPPID#include stdio.h #include sys/types.h #include unistd.h int main(){ printf(pid:%d\n,getpid()); printf(ppid:%d\n,getppid()); return 0; }3.1.5通过系统调用创建进程-fork●运行man fork认识fork●fork有两个返回值●父子进程代码共享数据各自开辟空间私有一份采用写时拷贝#include stdio.h #include sys/types.h #include unistd.h int main(){ int retfork(); printf(hello proc:%d,ret:%d\n,getpid(),ret); sleep(1); return 0; }●fork之后通常要用if进行分流#include stdio.h #include sys/types.h #include unistd.h int main(){ int retfork(); if(ret0){ perror(fork); return 1; } else if(ret0){ printf(I am child:%d,ret:%d\n,getpid(),ret); } else{ printf(I am father:%d,ret:%d\n,getpid(),ret); sleep(1); return 0; }3.2进程状态3.2.1Linux内核源代码●为了理解正在运行的进程是什么意思需要知道进程的不同状态。一个进程可以有几个状态在Linux内核里进程有时候也叫做任务下面的状态在kernel源代码里定义/* *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这个状态只是一个返回状态你不会在任务列表中看到这个状态。3.2.2进程状态查看ps aux / ps axj 命令●a显示一个终端所有的进程包括其他用户的进程●x显示没有控制终端的进程例如后台运行的守护进程●j显示进程归属的进程组ID、会话ID、父进程ID以及与作业控制相关的信息●u以用户为中心的格式显示进程信息提供进程的详细信息如用户、cpu和内存使用情况等3.2.3 Zzombie-僵尸进程●僵尸状态Zombies是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程●僵尸进程会以终止状态保持在进程表中并且会一直在等待父进程读取退出状态代码●所以只要子进程退出父进程还在运行但父进程没有读取子进程状态子进程进入Z状态创建一个僵尸进程#include stdio.h #include stdlib.h int main() { pid_t id fork(); if(id 0){ perror(fork); return 1; } else if(id 0){ //parent 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; }编译并在另一个终端下监控开始监控结果Ptrace 详解 - tangr206 - 博客园3.2.4僵尸进程的危害●进程的退出状态必须被维持下去因为它要告诉它的进程父进程你交给我的任务我办的怎么样了。可父进程若一直不读取那子进程就一直处于Z状态●维护退出状态本身就是要用数据维护也属于进程基本信息所以保存在task_structPCB中换句话说Z状态一直不退出PCB一直都要维护●那一个父进程创建了很多子进程就是不回收会导致内存资源的浪费。因为数据结构对象本身就要占用内存像C语言中定义一个结构体变量对象是要在内存的某个位置进行开辟空间●导致内存泄露3.2.5孤儿进程●父进程若提前退出那么子进程后退出进入Z之后该如何处理●父进程先退出子进程就称为“孤儿进程”●孤儿进程被1号init/systemd进程领养由init/systemd进程回收#include stdio.h #include unistd.h #include stdlib.h int main() { pid_t id fork(); if(id 0){ perror(fork); return 1; } else if(id 0){//child printf(I am child, pid : %d\n, getpid()); sleep(10); }else{//parent printf(I am parent, pid: %d\n, getpid()); sleep(3); exit(0); } return 0; }3.3进程优先级3.3.1基本概念●CPU资源分配的先后顺序就是进程的优先权priority●优先权高的进程有优先执行权力。配置进程优先权对多任务环境的linux很有用可以改善系统性能●还能把进程运行到指定的CPU上把不重要的进程安排到某个CPU可以大大改善系统性能3.3.2查看系统进程在linux或unix系统中用ps-l命令则会输出以下内容其中●UID执行者的身份●PID进程的代号●PPID这个进程是由哪个进程发展衍生而来的即父进程的代号●PRI这个进程可被执行的优先级其值越小越早被执行●NI这个进程的nice值3.3.3PRI and NI●PRI即进程的优先级或者说程序被CPU执行的先后顺序此值越小进程的优先级越高●NI即nice值表示进程可被执行的优先级的修正数值●PRI越小越快被执行加入nice值后将会使PRI变为PRInewPRI默认80nice●当nice值为负值时该程序优先级值将会变小优先级变高越快被执行●所以调整进程优先级在Linux下就是调整进程nice值●nice其取值范围是[-2019]共40个级别3.3.4PRI vs NI●进程的nice值不是进程的优先级它们不是一个概念但进程nice值会影响到进程的优先级变化●nice值是进程优先级的修正数据3.3.5查看进程优先级的命令用top命令更改已存在进程的nice●top●进入后按“r” -输入进程PID-输入nice值注意●其它调整优先级的命令nice未运行renice正在运行●系统函数#include sys/time.h #include sys/resource.h int getpriority(int which, int who); int setpriority(int which, int who, int prio);3.3.6竞争、独立、并行、并发●竞争性系统进程数目众多而CPU资源只有少量甚至1个所以进程之间是具有竞争属性的。为了高效完成任务更合理竞争相关资源便具有了优先级●独立性多进程运行需要独享各种资源多进程运行期间互不干扰●并行多个进程在多个CPU下分别同时进行运行●并发多个进程在一个CPU下采用进程切换的方式在一段时间之内让多个进程都得以推进3.4进程切换CPU上下文切换其实际含义是任务切换或CPU寄存器切换。当多任务内核决定运行另外的任务时它保存正在运行任务的当前状态也就是CPU寄存器中的全部任务。这些内容被保存在任务自己的堆栈中入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器并开始下一个任务的运行这个过程就是context switch参考一下Linux内核0.11代码注意时间片当地计算机都是分时操作系统每个进程都有它合适的时间片其实就是一个计数器。时间片到达进程就被操作系统从CPU中剥离下来3.5Linux2.6内核进程O(1)调度队列上图是Linux2.6内核中进程队列的数据结构3.5.1一个CPU拥有一个runqueue若有多个CPU就要考虑进程个数的负载均衡问题3.5.2优先级●普通优先级100~139我们都是普通的优先级与nice值的取值范围对应●实时优先级0~99不关心3.5.3活动队列●时间片还没有结束的所有进程都按照优先级放在该队列●nr_active共有多少个运行状态的进程●queue[140]一个元素就是一个进程队列相同优先级的进程按照FIFO规则进行排队调度所以数组下标就是优先级●从该结构中选择一个最合适的进程过程1.从0下标开始遍历queue[140]2.找到第一个非空队列该队列必定为优先级最高的队列3.拿到选中队列的第一个进程开始运行调度完成4.遍历queue[140]时间复杂度是常数但是还是太低效●bitmap[5]共140个优先级一共140个进程队列为提高查找非空队列的效率就可以用5*32个比特位表示队列是否为空这样就可以大大提高查找效率3.5.4过期队列●过期队列何活动队列结构一模一样●过期队列上放置的进程都是时间片耗尽的进程●当活动队列上的进程都被处理完毕之后对过期队列的进程进行时间片重新计算3.5.5active指针和expired指针●active指针永远指向活动队列●expired指针永远指向过期队列●可是活动队列上的进程会越来越少过期队列上的进程会越来越多因为进程时间片到期时一直都存在●在合适的时候只要能交换active指针和expired指针的内容就相当于具有了一批新的活动进程3.5.6总结在系统当中查找一个最合适调度的进程的时间复杂度是一个常数不随着进程增多而导致时间成本增加称之为进程调度O1算法代码块 struct rq { spinlock_t lock; /* * nr_running and cpu_load should be in the same cacheline because * remote CPUs use both these fields when doing load calculation. */ unsigned long nr_running; unsigned long raw_weighted_load; #ifdef CONFIG_SMP unsigned long cpu_load[3]; #endif unsigned long long nr_switches; /* * This is part of a global counter where only the total sum * over all CPUs matters. A task can increase this counter on * one CPU and if it got migrated afterwards it may decrease * it on another CPU. Always updated under the runqueue lock: */ unsigned long nr_uninterruptible; unsigned long expired_timestamp; unsigned long long timestamp_last_tick; struct task_struct *curr, *idle; struct mm_struct *prev_mm; struct prio_array *active, *expired, arrays[2]; int best_expired_prio; atomic_t nr_iowait; #ifdef CONFIG_SMP struct sched_domain *sd; /* For active balancing */ int active_balance; int push_cpu; struct task_struct *migration_thread; struct list_head migration_queue; #endif #ifdef CONFIG_SCHEDSTATS /* latency stats */ struct sched_info rq_sched_info; /* sys_sched_yield() stats */ unsigned long yld_exp_empty; unsigned long yld_act_empty; unsigned long yld_both_empty; unsigned long yld_cnt; /* schedule() stats */ unsigned long sched_switch; unsigned long sched_cnt; unsigned long sched_goidle; /* try_to_wake_up() stats */ unsigned long ttwu_cnt; unsigned long ttwu_local; #endif struct lock_class_key rq_lock_key; }; /* * These are the runqueue data structures: */ struct prio_array { unsigned int nr_active; DECLARE_BITMAP(bitmap, MAX_PRIO1); /* include 1 bit for delimiter */ struct list_head queue[MAX_PRIO]; };