精华内容
下载资源
问答
  • 什么程序,进程和线程?三者之间有何关系?

    万次阅读 多人点赞 2017-05-10 19:44:11
    进程进程与线程的历史我们都知道计算机是由硬件和软件组成的。硬件中的CPU是计算机的核心,它承担计算机的所有任务。 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配、任务的调度。 ...

    进程

    进程与线程的历史

    我们都知道计算机是由硬件和软件组成的。硬件中的CPU是计算机的核心,它承担计算机的所有任务。 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配、任务的调度。 程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等。 每次执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页,为了保证其独立性,就需要一个专门的管理和控制执行程序的数据结构——进程控制块。 进程就是一个程序在一个数据集上的一次动态执行过程。 进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。

    在早期的操作系统里,计算机只有一个核心,进程执行程序的最小单位,任务调度采用时间片轮转的抢占式方式进行进程调度。每个进程都有各自的一块独立的内存,保证进程彼此间的内存地址空间的隔离。 随着计算机技术的发展,进程出现了很多弊端,一是进程的创建、撤销和切换的开销比较大,二是由于对称多处理机(对称多处理机(SymmetricalMulti-Processing)又叫SMP,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构)的出现,可以满足多个运行单位,而多进程并行开销过大。 这个时候就引入了线程的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合 和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。 线程没有自己的系统资源,只拥有在运行时必不可少的资源。但线程可以与同属与同一进程的其他线程共享进程所拥有的其他资源。

    ## 进程与线程之间的关系
    线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一内存空间,当进程退出时该进程所产生的线程都会被强制退出并清除。线程可与属于同一进程的其它线程共享进程所拥有的全部资源,但是其本身基本上不拥有系统资源,只拥有一点在运行中必不可少的信息(如程序计数器、一组寄存器和栈)。

    重点:

    • 进程:

    优点: 同时利用多核cpu,能够同时进行多个操作

    缺点:耗费资源(每个进程内核都要为其开辟线性地址空间,概念清看下面的)

    • 线程:

    优点:共享内存,I/O操作可以并发,比如爬虫

    缺点:抢占资源,内核会枷锁,容易造成死锁

    1. 子进程不是越多越好,跟cpu核数相近或处理速度够快,根据处理速度进行启动,太多的话,除了资源浪费,还有进程上下文切换,cpu的中断interrupt,如果涉及到io操作的话还有模式转换
    2. 子线程也不是越多越好,进程上下文切换,cpu的中断interrupt
    3. 计算机最小的任务执行单元:线程。说的微线程:协程 后面有介绍,其实就是控制线程的执行位置
    4. I/O操作不占用cpu时间,而是发生用户内核两种模式转换
    5. I/O密集型应用(cpu) ======= 多线程
    6. 计算密集型应用(用cpu) =========多进程
    7. python的进程上有个GIL 全局解释性锁,这个会造成,一个进程的多个线程,不能同时使用多个cpu,而是cpu每次只能选一个线程执行,因此,多线程在cpu执行的是无效的。但是在I/O操作的时候是可以同步的,比如time.sleep就是io 操作,多线程,可以同时等待
    主线程
    比如我们写的py文件,执行的时候,所有代码是如何向下执行呢?肯定有个主线程的。
    我们创建多线程时候,这些线程都是子线程,那肯定有个主线程的。

    进程 和 线程的概念

    进程和程序关系

    进程
    程序实例 程序子集 有所谓生命周期,可以kill叼 比如你安装的word 是一个程序 ,你打开一个文档是一个进程,可以关掉。
    进程要想完成并发执行的功能就要进程切换
    进程切换 ,上下文切换,进程运行,说明在cpu的寄存器里面有数据了。假如5条数据现在有两条,就切换了,现在要保存现场,回来时候 要恢复现场。如果机器上有几千个进程,会切换 上万个切换需要时间,进程切换时监控程序来完成的,也就是内核,消耗时间正常程序执行空间是用户空间,占用在内核,说明大量时间消耗到进程切换。不好。

    首先我们知道cpu是执行二进制的指令的,而对应的就有两种编程:解释形语言 和 编译形语言 两种编程, c++就是编译成二进制一起执行的,所以c++是编译形 解释型:JavaScript等。所以cpu是执行指令流的。

    进程
    由父进程 fork 自身而来
    ls 这个进程的父进程是 shell frok而来父进程 启动子进程,子进程内存空间是在父进程空间,一旦外部加载数据会重新开辟一个内存空间程序时由指令和数据组成。程序时位于硬盘上的,是死的,只有当内核创建数据结构,分配了数据资源,cpu资源,处于活动状态,才有真正的执行价值,才会被拿来一个个被运行。程序内的指令能不能并行执行?即进程内的多条指令流。单核是不能了。双核的话,要是第一条运行到一半依靠第二个指令的结果,所以不能同时运行的。这是一个执行流的情况下。

    cpu在执行程序时,什么叫进程?

    • 出现内核之前

    进程在发起之前,是一个程序,在磁盘上。比如我们ls 多个用户可以发起多个ls进程。不互相干扰,不会意识到彼此存在。进程是程序的副本。
    程序本来在硬件运行。但是指令+数据在内存中。 所以cpu会执行一条拿一条,为什么要用内核呢?我们程序在cpu运行,再完之前,不会退出的, 我想让另一个程序在cpu运行,是不行的。
    比如第一个在执行时,可能会产生I/O,过程中比如需要打开一个文件,这个文件在磁盘上很大,而程序必须将这个文件内容加载进内存的这个程序的数据区域之中。硬盘速度很慢。我们cpu这个时间很快,就加载一下就休息一下。cpu就闲置了。
    我们为了解决多个问题,就在cpu执行多任务了,同时执行。内存中有多个程序了。那第一个加载数据了,第二个怎么执行呢?抢过cpu?就打架了,而内存也可能打架,这样就需要一个监控工具在上层。所以内核这个资源调度监控工具。
    linux 本身就是抢占式多任务。

    • 出现内核之后

    有内核这个监控调度工具:内核就负责这样几项: 以后这个程序再想执行,是直接向内核注册申请的,内核决定,你可以使用cpu,使用2ms,分配给你,不管2ms以后你有没有结束,都要交给另一个程序。这样轮询交替,让用户看起来是同时运行的。其实在同一时间只能有一个程序来占用一个cpu运行的。

    • 所以,进程的启动调度都是由内核完成的。
      在没有 内核之前,程序运行在内存。是作业。
    • 后来为甚么叫进程呢?

    内核必须把有限资源分配个各个贪婪的程序,会把cpu切割成片,时间片,我们cpu是按照时间流逝进行完成的。内存是拿空间用来存储。cpu是时间
    内核也是程序,所以有内核空间,用户程序,在用户空间,用户程序的单独空间是可以伸缩的。都直接把程序的空间连续起来是有弊端的,伸缩,会导致很多内存碎片。以后再想找连续空间就没了

    • 内存单位:页面
      我们大多数内存空间叫分页内存空间,跟我们磁盘一样,也是分块的。叫内存页面 而能存数据的叫内存页框 page frame

    MMU:内存控制单元,其实是星型方式才具备的组件,尤其是x86架构。在arm上有没有不知道。主要是以x86架构的cpu说明的
    mmu:memerry manage unit

    mmu 出现的主要目的是为了实现内存分页

    我们应该知道,一个程序被加载到内存,并不是所有指令在运行,比如ls /etc 跟ls -l /etc 这个ls 的执行代码都不一样。所以我们的ls 程序不是所有指令都加载进去的。需要-l再把-l的指令加载进来。 数据量不同。存的页框不同。需要1个我们分1个,需要2个分2个页框,不够了,找其他页框,即便不连续也可以,最好连续。 只要我们内核追踪着这个进程知道这个内存页框就行。
    我们内核,都给进程虚拟出了一段假的内存空间,让进程看起来是连续的。
    一个进程最多能识别多大内存呢?
    32位 4G
    64位 4G^4G

        一个程序的4G内存空间,它的所有执行操作都是通过调用内核中的系统调用来完成的。
        后来编程的时候不是在cpu上使用指令集编程的。而是使用系统调用。
        所以进程是知道内核存在的,所以进程要跟内核和平共处。
    

    线性地址空间:
    因此32位的这4g虚拟 空间,有1g分给内核,剩下的程序自己用。而实际上我们的物理内存只有512M。但是为了统一管理,为了使得进程能够以统一风格,任程序员编程,不管你是512M物理内存还是1g物理内存,都是虚拟出4g内存,1g分给内核,其他分给程序的。这段空间是假的,是内核虚拟给进程的。这段地址空间被称为线性地址空间,看上去连续的,其实不连续。
    我们的3g开始是程序的执行入口的,开始一段是记录文件的格式等内容,前面是为空的
    真实内存比如512M,但是会虚拟出4G线性空间给每个进程,每个进程都会以为4G虚拟空间,但是 不会用4G,该用多少用多少。

    怎么使用这4G空间呢?\

    刚一直在讲,程序是由指令加数据组成的,这个空间内放着程序的指令和数据,除了内核空间外。光有指令和数据 还不够,实际上在物理内存中是可能不连续的页框,线性地址空间的对应指令指向物理内存的不连续的页框。这样单个进程看到的虚拟空间只是 自己单独存在的。各自不互相影响。

    swap 内存的缺页异常 大的异常

    这样总有一天内存耗尽了。就会将内存页面页框内的 数据转移到我们硬盘上,叫做swap交换分区。用于临时存放内存中暂时用不上的内存页面。用的什么算法呢?lru算法,lru:最近最少使用的内存页面数据。但是如果人家进程醒了,访问被拿走的数据,内核会阻止它,告诉它没有,把它的拿回来从swap内,再把别人的腾空放到swap内。这就是所谓的缺页异常。 我找的页面不在了。我的内容本来在内存中,但是内存中不在了。这种情况就会产生I/O。为什么产生I/O呢?因为内核要从磁盘上抢回数据,从swap内。与磁盘操作就产生I/O,这就是大的异常。
    

    小的异常 内存映射,mmap

    我们知道,程序在执行时,都是动态链接的方式链接到好多个库上的。如果程序运行要依赖这个库,就会把这个库加载到内存上来的,因为程序的某些功能要依赖这个库的执行来实现的。但是这个库叫so ,共享对象,多个进程可以共同使用的。比如进程1 需要 lib1加载到内存的一个页面上。进程2启动也需要依赖lib1,库文件都是执行的代码,不会改变。
    进程2 通知内核加载lib1,由于lib1已经由进程1加载到内存在,不会再次加载,但是进程2没有虚拟内存对应的页面,就会出现缺页异常,这时内核就会说,等一下,内存中有已经加载了lib2,安抚一下,(当然不是其独享的。如果其他进程需要依赖某个库,就会映射到哪个进程的空间)   将加载的页面告诉进程2.
    而lib2加载到内存的空间由两个进程共享,都以为是自己的。但是不会 随着一个 结束而释放,只有全部释放才 释放。而这段共享空间是,内存映射,mmap==memery map。 这段内存是共享内存空间,是映射到进程里面去的。
    

    内存泄露:

    对于linux用户空间中,init是所有进程的老祖宗。除了init之外,其他进程如果需要其他任务,就会创建子进程。子进程都是由其父进程通过向内核fork系统调用复制的,父进程通过自身fork一个一模一样的子进程除了。由子进程完成特定任务,一旦子进程完成任务,就清理子进程。kill子进程,终止子进程
    一旦父进程都over了,如果还有子进程。所有子进程都成孤儿了。所有进程的父进程是init。父进程在over之前,要给子进程找一个新爹,即监护进程。如果没有给子进程找新爹,这个子进程就成孤儿了,所有的孤儿进程就会被回收了,无法被回收占用的内存就无法被回收了,无法被分配了。这样分配出去的内存无法回收旧造成内存泄露了。这就找程序员了。程序问题。重启可以,内存重新分配。  因为我们无法追踪孤儿进程,无法kill。
    linux用户空间中,init是所有进程的老祖宗。除了init之外,其他进程如果需要其他任务,就会创建子进程。子进程都是由其父进程通过向内核fork系统调用复制的,父进程通过自身fork一个一模一样的子进程除了
    

    task_struc

    就算是父进程kill杀掉子进程,也会像内核去申请终止 才能终止。因此,在内核中为进程创建了信息库,户口簿,
    记录了每一个进程的  进程号,进程名  进程父进程  进程使用的cpu时间累积 分配的内存也,父进程编号,占用了哪些cpu 等等。监视无处不在。这些对于linux内核叫 task_struct,任务结构。每一个任务都有一个结构,所以每创建一个子进程,都会向内核注册一个任务,内核创建一个任务结构,来保存任务的各相关属性信息,内核从而追踪他所使用的各相关资源了。
    

    双向循环的链表

    内核这种任务有n个,那内核是如何找到每一个呢?
            这些数据结构,是c实现的。python简单的多。单c的效率高。
            着每个结构对编程语言来讲是数据结构。描述 数据的属性信息。 进程号 id  父进程等属性信息。这种描述进程格式叫数据 结构。这种数据结构用c描述要复杂的多。
            每个进程有一个任务结构,那我们内核怎么知道有多少个呢,如果方便的找到任意一个ne ?
            而我们的linux 的内核也是程序,而程序也要 存储到内存当中。内核也要在cpu运行之后,才能存到内存中的。内核要自身追踪这些结构,是通个c语言的双向循环的链表 实现的。每个结构也有结构的编号。
            第一个结构尾部记录第二个结构的内存地址编号。而第二个结构首部 存有第一个结构的地址编号。
     
     
    没有进程查看工具之前,我们要查看系统有多少个进程,就遍历上图的列表计算一次。
    

    进程属性在linux系统的存放位置:

     内核不能随便访问。所以我们需要伪文件系统。所以每个进程的进程号在proc下。这些个进程号下存着各种属性数据。启动命令,那个cpu运行过,哪些内存。
     
    进程管理工具实现:
            我们所谓的进程查看工具,就是获取为文件系统的进程属性的。
     
     
    内存中,有1g是我们内核空间的,用户空间存放我们要运行程序的指令,cpu执行指令的方式有三种,顺序、循环、选择这三种执行。
     
            比如一个进程申请的4g空间,内核占1g,而程序指令在用户空间,cpu依次读取进行,当执行到一个点无法完成,就会生成一个子进程,由于同一时间1个cpu只能做1件事情,cpu是时间片切片机制,这时父进程就会休眠。而子进程又会申请开辟一个4g的线性地址 空间,cpu又会继续执行子进程的指令,执行完后,结构返回给父进程。
            即使有两个cpu 程序也不能即使执行。因为父进程等着子进程返回结构才能继续工作。
    

    线程: thread

    程序时由指令和数据组成。程序时位于硬盘上的,是死的,只有当内核创建数据结构     ,分配了数据资源,cpu资源,处于活动状态,才有真正的执行价值,才会被拿来一个个被运行。
        程序内的指令能不能并行执行?即进程内的多条指令流。单核是不能了。双核的话,要是第一条运行到一半依靠第二个指令的结果,所以不能同时运行的。这是在一个执行流的情况下。
            那我们两个cpu干什么呢?可以执行多个进程啊,虽然不能执行一个进程的多个指令
    分成n个执行流,每个流内都有指令和数据,是不互相干扰的。
    
    这样就需要一个进程空间内部,并行执行。一个进程内部有多个执行流来并行操作,但是出现资源争抢线性
            线程是一个进程内部的多个执行流
                多个线程不能同时打开一个资源文件,而lib尽管可以共享,也是在内核级别存在的。
                资源发送争抢的区域叫临界区。
            单进程、单线程:一个进程内存空间内有一个执行流。
            单进程、多线程:一个进程内的指令集分成多个执行流
     
            不管你的一个进程空间内部有多少个执行流,说白了就是多少个线程,但是你有一个cpu,多个线程没有什么明显的地方,除了麻烦以外。
            多个cpu就有好多好处了。
    
    web服务器工作模型及短板。:
            web服务器:web服务进程。有一个用户访问时,我不能直接访问,否则有问题,那第二个来访问怎么办,只能等着。    
            怎么办呢?给每个用户生成一个子进程,每个用户一个子进程,一个用户需要10M内存,1000个用户就是10g。还要涉及到进程切换。但是如果访问主页,主页这个内存空间的话,每个用户这一个子进程就要开辟一块,那我们的服务器资源都要撑坏了。所以为了让众多用户共享一个资源来说,我们就要采用线程模型。web服务比如启动一个进程,里面分配多个线程,这样我们就打开一份数据就行了。cpu多的话就好了。
            但是多个I/O争用的。虽然我们cpu多个,里面处理数据很快,但是我们网卡就有一个,还是会堵在网卡队列这里。
    所以我们要理解原理,我们就知道系统瓶颈短板在哪了。
            
     
    对于linux 而言,不是线程操作系统。windows solorias 都是微内核的线程操作系统。linux是单内核的。
     
    线程跟内核本身的关系并不大,是由线程库实现的。迄今为止,linux内核中维持的每一个线程,跟进程毫无区别。
    

    LWP

    就算你生成了n个线程,但是在 linux看来跟进程是一样的。只不过是占用内存大小变小,称为轻量级进程。LIGHT WEIGHT PROCESS
    linux 原生内核不支持线程,好多爱好开发者提供了好多实现方式,linux内核实现一套   红帽  一套   其他一套  三种实现需要线程库的支持。
     
     线程死锁
    线程不一定是好事,多个线程不能同时抢占一个文件。内核会给资源枷锁。
    线程资源浪费:
        死锁:1拿2的资源 2拿1的资源,都在等着释放。
               1 等着2拿的资源,cpu过一下看一回,一直cpu看,浪费时间。
        liunx内部有自选锁。很复杂
    

    模式转换:内核模式 用户模式

    为了避免用户的进程操作系统资源,cpu是有保护进制的。cpu把它能操作运行的指令时分4类。分别放在环0 1 2 3 上。
    用户进程只要不是以内核方式开发的,是以自己编程方式开发的基本程序,是不能调用特权指令,一旦尝试调用cpu的特权指令,cpu就会唤醒内核的。而普通程序要想访问硬件,就要访问特定的程序接口访问特权指令,要通过系统调用通知内核。
            库调用,系统调用。
            库调用是执行库的代码的,而系统调用是执行内核代码的。
            程序运行,到内核运行 在回到用户空间这就叫做模式转换。
     
     
            所以产生系统调用在内核浪费的时间越多,cpu越浪费。所以大部分时间70%应该浪费到用户空间,才会产生生产力
    比如我们的ftp服务器,只有在访问硬盘数据资源,网卡封包解包等才会执行内核模式操作。所以浪费在内核模式的程序大部分是系统程序。而用户工作程序,应该大部分时间需要浪费在用户空间。
    

    进程切换: 上下文切换 进程的环境切换

    cpu只有一颗,在同一时刻,cpu运行的进程只有一个,不能让一个进程一直占用cpu,我们一定要让其及时切换交给其他进程。
             寄存器:保存当前进程的执行状态码
             1级缓存  2级缓存 3级缓存 内存  硬盘
     
            而进程切换,我们要把寄存器当前的执行状态码,保存下来。保存现场。
            每个进程,内核都会在内存维护一个task_starck 任务结构,用于保存进程的属性状态信息。一旦切换进程,就会保存当前状态。再把另一个进程,本来保存在内存的任务结构的状态信息,从新交给cpu执行。在这个切换时间内,cpu并没有干活。所以这个切换时间越快越好。
            那是越频繁越好呢??  大量时间cpu都浪费到来回切换了。
    

    时钟中断

    当然内核也有内核工作频率的,靠时钟驱动的。比如内核100hz/s   1s 震荡100次。内核工作hz 200 500 1000  s是越快越好吗?
            每次HZ翻转时,时钟震荡翻转时,都会产生时钟中断发生1s,linux是抢占式多任务,如果中断就会发生抢占在时钟中断周期走完时。
     
            cpu时间走完了,或者内核分配的时间走完了。
     
            为什么会抢?
                进程是有优先级的。
                进程调度,是内核的核心功能之一。要公平合理。对于那些需要占用特权资源的。紧急的进程优先级高。
            
                公平:
                        结果公平
                        七点公平
     
                进程的调度算法非常重要:
                        统筹方法
                
                        程序=算法指令+数据 结构
     
     
                    优先级高的排前面,低的排后面。排1对合适吗?
                    我们按照优先级排队,优先级相同的排一队。在算法过滤
     
     
                    Big O :算法优先的判断工具
                    O(1)
                        横 队列长度   纵是扫描时间
    

    内核的功用:进程管理、文件系统、网络功能、内存管理、驱动程序、 安全功能等

    Process: 运行中的程序的一个 副本,是被 载入内存的一个指令集合

    进程ID (Process ID ,PID )号码被用来标记各个进程
    UID 、GID 、和SELinux 语境决定对文件系统的存取和 访问权限,通常 从执行进程的用户来继承存在生命周期

    task struct :Linux 内核存储进程信息的数据结构格式

    task list :多个任务的的task struct 组成的链表

    进程创建:

    init :第一个进程
    父子关系
    进程:都由其父进程创建,CoWfork(), clone()

    进程优先级:

    • 系统优先级: 数字越小,优先级越高
    • 0-139 (CentOS4,5)各有140 个运行队列和过期队列
    • 0-98 ,99 (CentOS6) )
      -实时优先级(rtpri): 99-0 :值最大优先级最高
      -nice 值:-20 到19 ,对应 系统优先级100-139 或99
    • cpu分配算法Big O :时间复杂 度 ,用时和规模 的 关系
      O(1), O(logn), O(n) 线性, O(n^2) 抛物线, O(2^n)
      -LRU :Least Recently Used 近期最少使用算法

    关注获取最新优质文章
    在这里插入图片描述

    展开全文
  • 设备驱动进程与设备控制器之间的通信程序称为设备驱动程序。 设备驱动程序是控制设备动作的核心模块,如设备的打开、关闭、读、写等,用来控制设备上数据的传输。它与硬件密切相关,处理用户进程发出的I/O请求。 ...
    设备驱动进程与设备控制器之间的通信程序称为设备驱动程序。 
    设备驱动程序是控制设备动作的核心模块,如设备的打开、关闭、读、写等,用来控制设备上数据的传输。它与硬件密切相关,处理用户进程发出的I/O请求。
    用户进程使用设备驱动程序时,设备驱动程序的处理过程为:将用户进程抽象的I/O要求转换为具体的要求,检查I/O请求的合法性,读出和检查设备的状态,传送必要的参数,设置设备工作方式,启动I/O设备。
    展开全文
  • Linux C程序修改进程名称

    千次阅读 多人点赞 2021-05-12 22:13:26
    Linux C程序修改进程名称(通过修改argv[0])1、前言2、命令行参数(argc, argv)以及环境变量(environ)介绍2.1、C程序典型的存储空间布局2.2、argc, argv介绍2.3、environ介绍2.4、编写程序验证修改进程名是否可以...

    1、前言

    Linux C程序运行时,进程的名称通常就是argv[0],而通过修改内存中argv[0]存储的内容就可以修改进程名了。下面对此作详细介绍。

    2、命令行参数(argc, argv)以及环境变量(environ)介绍

    2.1、C程序典型的存储空间布局

    如图所示,是C程序典型的存储空间布局,可以很明显看到命令行参数环境变量处在最顶端,而且是挨在一起的,命令行参数后面紧接着就是环境变量,具体的内容大家可以自行看一下《UNIX环境高级编程》第七章,下图也是从里面摘录出来的
    在这里插入图片描述

    2.2、argc, argv介绍

    每个C语言程序都必须有一个称为main的函数,一般形式为int main(int argc, char* argc[]),改函数作为程序启动的起点。执行程序时,命令行参数通过argcargv这两个两个入参传递给main函数。第一个表示命令行参数的个数,而第二个参数则是一个指向命令行参数的指针数组,每一参数都是C类型字符串,以空字符\0结尾的,而最后一个参数argc[argv]则是一个空指针NULL。其中第一个字符串(argv[0]),则是该程序的名称,也就是运行时的进程名。

    内存示例如图所示
    在这里插入图片描述

    2.3、environ介绍

    每一个进程都有与其相关的称为环境列表(environment list)的字符串数组。其中每个字符串都以名称=值(name=value)形式定义。新进程在创建时,会继承其父进程的环境副本,子进程创建后,父、子进程均可更改各自的环境变量,且这些变更对对方而言不再可见。
    在C程序中,可以使用全局变量extern char** environ获取环境变量表(C运行时启动代码定义了该变量并以当前环境列表位置为其赋值)environargv参数比较相似,是一个指针数组,数组每个成员指向C类型字符串,最后一个指针也是一个NULL

    内存示例如图所示
    在这里插入图片描述

    2.4、编写程序验证修改进程名是否可以成功

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    
    int main(int argc, char* argv[])
    {
        sleep(8);
        strcpy(argv[0], "112233445566");
        while (1);                                                                                                           
     
        return 0;
    }
    

    代码如上,可以看到,程序先休眠8秒钟,然后修改argv[0]的值,编译运行一下,运行结果如下,可以看到,一开始进程名是./all,8秒后就变成了112233445566,修改成功,而且我们也能看出一点细节,那就是修改后的进程名超过了原先的进程名也没有报错,有可能的原因是argc[0]后面的内存也是可用的(其实从内存布局就可以看出来可用了)
    在这里插入图片描述

    2.5、查看进程名变长之后影响了那部分内存的数据

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    
    extern char** environ;
    
    int main(int argc, char* argv[])
    {
        printf("begin environ:\n");
        for (int i = 0; environ[i] != NULL && i < 5; i++)
            printf("%s\n", environ[i]);
    
        sleep(5);
        strcpy(argv[0], "112233445566");
        printf("\n\n");
    
        printf("end environ:\n");
        for (int i = 0; environ[i] != NULL && i < 5; i++)
            printf("%s\n", environ[i]);
    
        return 0;
    }
    

    代码如上,为了方便显示,环境变量只打印前五个,看看效果就行。运行结果如下图所示,可以看到,一开始打印的第一个环境变量为XDG_SESSION_ID=3585,当修改完进程名之后,该变量变为445566,原先进程名为./all,加上结束符是6个字节,剩余四个字节的内容也就是445566刚好把第一个环境变量的内存给覆盖了(这里没有传入参数,所以覆盖的是环境变量的内存)。
    在这里插入图片描述

    2.6、结论

    Linux C程序中传入参数和环境变量是存放在一段连续的内存空间的,而argvenviron这两个指针数组里面存放的指针就是指向这些内存的

    3、Redis修改进程名的做法

    Redis作为一个优秀的企业级存储框架,里面有许多值得学习的地方,而里面也有一段代码修改进程名的,接下来看看Redis是如何实现的

    extern char **environ;
    
    static struct {
    	/* original value */
    	const char *arg0;
    
    	/* title space available */
    	char *base, *end;
    
    	 /* pointer to original nul character within base */
    	char *nul;
    
    	_Bool reset;
    	int error;
    } SPT;
    
    void spt_init(int argc, char *argv[]) {
        char **envp = environ;
    	char *base, *end, *nul, *tmp;
    	int i, error, envc;
    
    	if (!(base = argv[0]))
    		return;
    
    	nul = &base[strlen(base)];
    	end = nul + 1;
    
    	for (i = 0; i < argc || (i >= argc && argv[i]); i++) {
    		if (!argv[i] || argv[i] < end)
    			continue;
    
    		if (end >= argv[i] && end <= argv[i] + strlen(argv[i]))
    			end = argv[i] + strlen(argv[i]) + 1;
    	}
    
    	for (i = 0; envp[i]; i++) {
    		if (envp[i] < end)
    			continue;
    
    		if (end >= envp[i] && end <= envp[i] + strlen(envp[i]))
    			end = envp[i] + strlen(envp[i]) + 1;
    	}
    	envc = i;
    
    	if (!(SPT.arg0 = strdup(argv[0])))
    		goto syerr;
    
    	if (!(tmp = strdup(program_invocation_name)))
    		goto syerr;
    
    	program_invocation_name = tmp;
    
    	if (!(tmp = strdup(program_invocation_short_name)))
    		goto syerr;
    
    	program_invocation_short_name = tmp;
    
        /* Now make a full deep copy of the environment and argv[] */
    	if ((error = spt_copyenv(envc, envp)))
    		goto error;
    
    	if ((error = spt_copyargs(argc, argv)))
    		goto error;
    
    	SPT.nul  = nul;
    	SPT.base = base;
    	SPT.end  = end;
    
    	return;
    syerr:
    	error = errno;
    error:
    	SPT.error = error;
    }
    

    从代码中可以看出以下几点:

    1. base指向的是argv[0]nul指向argv[1],而end经过两个for循环遍历之后指向的是传入参数+环境变量内存空间的最末尾
    2. 使用strdup函数将原本的进程名复制给SPT.arg0
    3. spt_copyenvspt_copyargs函数的作用是将原先的传入参数(从argv[1]开始)和环境变量复制到一个新的内存区域,然后原先的指针数组里元素指向新的内存地址,完成了数据的迁移
    4. 经过上面的步骤之后,argv[0]还是指向原来的地址,而其他的传入参数以及环境变量均指向新的内存空间了,现在就可以任意修改进程名而不影响其他数据了

    4、总结

    本文介绍了如何修改Linux C程序的进程名,以及Redis源代码中是如何在保持原有传入参数以及环境变量的情况下,对程序的进程名进行修改。测试代码比较简单,大家可以自行编写程序去进行测试验证

    展开全文
  • 而这个新创建出来的进程称为进程的子进程,原进程称为进程的父进程。 该函数其实是一个系统调用接口,原型如下: #include <unistd.h> pid_t fork(void); 特性:子进程会复制父进程的PCB,二者之间...

    1. 进程创建

    1.1 fork

    在Linux中,我们通常使用fork函数来为一个已经存在的进程创建一个新进程。而这个新创建出来的进程被称为原进程的子进程,原进程被称为该进程的父进程。

    该函数其实是一个系统调用接口,原型如下:

    #include <unistd.h>
    pid_t fork(void);
    

    特性:子进程会复制父进程的PCB,二者之间代码共享,数据独有,拥有各自的进程虚拟地址空间

    此时可能会有一个疑问,既然代码共享,并且子进程是拷贝了父进程的PCB,虽然他们各自拥有自己的进程虚拟地址空间,但其中的数据必然是相同的(拷贝而来),并且通过页表映射到同一块物理内存中,那么又如何做到数据独有呢?答案是:通过写时拷贝技术。

    写时拷贝技术:子进程创建出来后,与父进程映射访问同一块物理内存,但当父子进程当中有任意一个进程更改了内存中的数据时,会给子进程重新在物理内存中开辟一块空间,并将数据拷贝过去。 这样避免了直接给子进程重新开辟内存空间,造成内存数据冗余。换句话说,如果父子进程都不更改内存中的值,那他们二者各自的进程虚拟地址空间通过页表映射,始终是指向同一块物理内存。

    正是通过这样的写时拷贝技术,才保证了父子进程代码共享但数据独有的这一特性。 对于一些小萌新来说,可能上述文字描述并不是那么直观,此处有必要上图来进一步说明一下:

    如父进程中有全局变量g_val初值为10,子进程创建之后通过复制父进程的PCB,并且二者的进程虚拟地址空间通过页表映射到同一块物理内存,但如果子进程更改了g_val的值,就会在物理内存中开辟新的空间并保存属于子进程的g_val:
    在这里插入图片描述

    在知道了以上特性后,下面我们来认识一下fork函数的返回值,相当重要!

    通过以上函数原型我们可以看到起返回值是pid_t类型,其实就是int,在内核中是通过typedef重命名过的,我们把其当做int类型即可。

    如果创建子进程失败,会返回-1,是小于0的,而如果创建子进程成功,该函数则会返回俩个值,这一点和普通的函数有很大区别。它会给子进程返回0值,而给父进程返回子进程的pid(一个大于0的数),也正是通过给父子进程返回值的不同,从而我们可以使用选择语句对齐进行分流,从而让父子进程执行不同的代码,而达到我们创建子进程的某种目的。

    在了解到这一点之后,我们便可以通过代码来创建子进程并且进一步验证前面说到的一些特性。

    #include <stdio.h>
    #include <unistd.h>
    
    //父子进程代码共享,但数据独有
    int g_val = 100;
    int main()
    {
        pid_t pid = fork();//创建子进程
        if(pid < 0) {
            printf("fork error!\n");
            return -1; 
        }
        else if(pid == 0) {
            //子进程
            g_val = 200;
            printf("This is Child! g_val = %d p = %p\n",g_val,&g_val);
        }
        else {
            //父进程
            sleep(1);
            printf("This is Parent! g_val = %d p = %p\n",g_val,&g_val);
        }
        return 0;
    }
    

    运行程序,得到如下结果:
    在这里插入图片描述
    对于这一结果感到惊讶吗?其实只要你看懂了我上面所说的内容,相信这个结果并不难理解:子进程拷贝父进程的PCB,拥有和父进程一模一样的进程虚拟地空间以及数据,但子进程将自己的g_val更改后,会在物理内存中为其重新开辟空间来存储子进程更改后的数据,而结果中看到的地址完全相同,则是因为它们仅仅是虚拟的地址空间,真正的值是存储在物理内存中的。而这时通过页表的映射,这俩个看似相同的地址已经指向了不同的物理内存。

    1.2 vfork

    不止可以通过fork来创建子进程,vfork也同样是用来创建子进程的系统调用函数,那么它和fork有什么区别呢?

    #include <sys/types.h>
    #include <unistd.h>
    pid_t vfork(void);
    

    通过函数原型我们似乎并不能看出什么端倪,的确,vfork在使用时和fork几乎没有什么区别,返回值及其含义也和fork完全相同。其和fork的区别在于,用v_fork创建出来的子进程,也是拷贝父进程的PCB,但它和父进程共享同一个进程虚拟地址空间。也就是如下图所示的这样:
    在这里插入图片描述
    但是我们要思考一个问题,父子进程共享同一个进程虚拟地址空间不会有问题吗?会的!会造成调用栈混乱的问题! 举个例子,如果父进程中调用Test函数首先压栈,之后子进程则调用Fun函数,由于二者共享同一个栈空间,则Fun函数也会继续压栈,但如果此时父进程的Test函数调用完毕想要返回,却发现其并不在栈顶位置,无法出栈,这不就有问题了吗?
    在这里插入图片描述

    那怎么解决呢?vfork采用的方案是,在其创建出子进程之后,让子进程先执行,而父进程则会阻塞,直到子进程执行完毕,父进程才会开始执行,这样就避免了调用栈混乱的问题。

    但是!这个问题是解决了,可是新的问题也随之而来了呀,我们创建子进程难道不是为了让其而父进程并发的跑或者说更高效的完成一些任务吗,而现在再子进程退出前父进程什么都不能做,这难道不会影响效率吗?或者说的再直白一些,不是浪费时间吗???

    不得不说,确实。可能也正是因为这些种种的缺点,vfork这个函数已然逐渐的被时代淘汰了,fork它不香吗?为什么要用vfork呢? 博主也理解不了它存在的意义…不过也罢,我们只需稍作了解,然后还是把爱全都给fork吧!
    在这里插入图片描述

    2. 进程终止

    含义:进程终止的含义就是一个进程的退出。

    进程退出的场景

    1. 程序运行完毕,从main函数中退出
      1.1 运行完毕,结果正确
      1.2 运行完毕,结果不正确
    2. 程序没有运行完毕,中途奔溃了

    进程常见退出方法:

    1. 正常退出:

    1. 从main函数返回
    2. 调用exit函数
    3. 调用_exit函数

    2. 异常退出: Ctrl+C,信号终止等

    exit函数:

    #include <stdlib.h>
    void exit(int status);
    

    其中,stauts定义了进程的终止状态,由用户自己传递,父进程可以通过wait来获取该值(下边进程等待部分实操)。

    _exit函数:

    #include <unistd.h>
    void _exit(int status);
    

    exit和_exit俩个函数都可以退出当前进程,而二者的区别在于:exit是库函数,_exit是系统调用函数,而库函数内部封装了系统调用。 也就是说,调用exit函数最终也会调用_exit来使进程退出,只不过在其调用_exit之前,还会做一些其他的事情,如下图:
    在这里插入图片描述
    从上图我们可以看出,exit()与_exit()还有一个很重要的区别就是在退出前会不会刷新缓冲区。显然,前者是会刷新缓冲区的,这也是它在封装后者的基础上所增加了一些后者并不具备的功能。

    代码验证如下:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main()
    {
      printf("我要退出了!\n");
      exit(1);
      printf("应该不会打印我了!\n");
      return 0;
    }
    

    运行以上代码,结果如下:
    在这里插入图片描述
    以上结果符合完全符合我们的预期,那如果使用_exit()呢?我们再试试:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main()
    {
      printf("我要退出了!");
      _exit(1);
      printf("应该不会打印我了!\n");
      return 0;
    }
    

    在这里插入图片描述
    不是说_exit()退出时不会刷新缓冲区吗?怎么还是会打印出来呢?注意:不是bug,原因是\n(换行符)也有刷新缓冲区的作用。我们去掉\n再次执行代码就会看到我们预期的结果:
    在这里插入图片描述
    那如何说明一开始调用exit不是因为其内部会刷新缓冲区而不是\n的作用呢?很简单,去掉\n再试试就清楚了,肯定也是会刷新缓冲区而打印对应内容的,只不过不会换行了。这里就不在演示。

    再补充一点,除了\n(换行)以及exit()函数会刷新缓冲区之外,也可以调用fflush()来强制刷新缓冲区

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    
    int main()
    {
      printf("我要退出了!");//没有\n
      fflush(NULL);//刷新缓冲区
      _exit(1);
      printf("应该不会打印我了!\n");
      return 0;
    }
    

    在这里插入图片描述
    以上结果与使用exit函数退出进程并且前一条打印语句不带\n一致,大家可以自行验证。

    3. 进程等待

    3.1 为什么要进程等待

    之前在了解进程概念的的时候有说到过僵尸进程,如果子进程先于父进程退出,而父进程并没有关心子进程的退出状况,从而无法回收子进程的资源,就会导致子进程变成僵尸进程。

    如果对信号有一定的了解,就会知道,僵尸进程一旦产生就算是kill-9这样的强杀信号都杀不掉它,因为谁也没办法杀掉一个已经死去的进程! 那怎么办呢?当时在进程概念的位置并没有提解决(避免)僵尸进程的办法,而在这个位置再次说到它,就是想来引出进程等待这个概念。进程等待的作用就是防止僵尸进程的产生!

    进程等待:父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出状态。

    那具体如何完成进程等待呢?答:在父进程中,使用wait或waitpid接口来完成进程等待。

    3.2 wait

    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *status);
    

    返回值:成功会返回被等待进程的pid,失败则会返回-1

    参数:一级指针status,它其实是个输出型参数,用于获取子进程的退出状态,如果不关心则可以设置为NULL

    代码实例:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main()
    {
      pid_t pid = fork(); //创建子进程
      if(pid < 0) {
        perror("fork");
        return -1;
      }
      else if(pid == 0) {
        //子进程
        printf("I am child, my pid is %p\n", getpid());
        sleep(3);
      }
      else {
        //父进程
        printf("I am father, my pid is %p\n", getpid());
        wait(NULL); //进程等待
        printf("进程等待成功!\n");
      }
      return 0;
    }
    

    在这里插入图片描述
    执行以上程序,等够成功等待使我们预期之内的,但我们还应知道的一点是,wait是一个阻塞接口,意味着它在等待子进程退出期间是阻塞在函数内部的,直到子进程退出,它获取了子进程的退出状态并回收子进程的资源,才会返回。 如果要验证以上结论,可以适当增加子进程中休眠的时间,然后使用pstack[父进程进程号] 查看调用堆栈就可以看出,这里不再进行验证。

    3.3 waitpid

    //头文件同wait的头文件
    pid_t waitpid(pid_t pid, int *status, int options);
    

    waitpid同样也可以被用来进行进程等待,但它较wait接口稍稍复杂一些:

    返回值:

    1. 等待成功正常返回则返回被等待进程的pid
    2. 如果第三个参数options设置成了WNOHANG,而此时没有子进程退出(没有成功等待到子进程),就会返回0,而不是阻塞在函数内部
    3. 调用出错则返回-1

    参数:

    1. pid,设置成-1则表示等待任意一个子进程,同wait;如果>0则表示等待一个指定的子进程,pid就是被等待子进程的进程号
    2. status,出参,获取子进程的退出状态,同wait
    3. options,可以设置为0或WNOHANG。设置为0则与wait一样,如果没有等待到子进程退出会一直阻塞;而设置为WNOHANG则表示非阻塞,如果被等待的子进程未退出,则会返回0值,成功等待到子进程则会返回被等待子进程的pid

    也就是说,如果使用waitpid接口并设置options参数为WNOHANG,则未等待到子进程退出时也会立即返回,而不是阻塞,因此这种场景我们一般搭配循环来使用,以确保可以成功等待到子进程退出。

    代码实例:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main()
    {
      pid_t pid = fork();
      if(pid < 0) {
        perror("fork");
        return -1;
      }
      else if(pid == 0) {
        //子进程
        printf("I am child, pid is %p\n", getpid());
        sleep(10);
      }
      else {
        printf("I am father, pid is %p\n", getpid());
        while(waitpid(pid, NULL, WNOHANG) == 0); //循环调用waitpid,直到其返回值不为0
        printf("进程等待成功!\n");
      }
      return 0;
    }
    

    运行结果:10秒之后waitpid成功等待到子进程退出而返回非0值,跳出while循环并执行后续打印语句
    在这里插入图片描述
    其他传参方式大家可以自行验证。

    3.4 获取子进程退出信息status

    我们发现,不论是wait还是waitpid都有一个出参status,而我们之前并未关心这一点,那么这里就来探讨一下如何获取子进程的退出状态吧!

    之前,我们已经知道status是一个出参,由操作系统为其赋值,用户可以传递NULL值表示不关心,而如果传入参数,操作系统就会根据该参数,将子进程的退出信息反馈给父进程,由status最终被赋予的值来体现。

    那么,到底如何通过status来获取子进程的退出信息呢,要知道这一点,我们必须先知道status的使用细节:

    status是一个int类型的值,意味着它应该有32个比特位,但它又不能被当初普通的整形来看待,因为其高16位的值并不被使用,而只使用其低16个比特位:
    在这里插入图片描述
    那么,在只关心其低16位的基础上,具体的比特位又分别代表什么含义呢,也就是如何通过这低16个比特位来获取子进程的退出信息呢,我们同样通过俩张图来解释:

    子进程正常退出时:
    在这里插入图片描述
    子进程异常退出时:
    在这里插入图片描述
    图片表达应该更加直观一些,不过还是要稍作解释:可以看出,不论是正常退出还是异常退出,status的高8个比特位(只讨论低16个比特位)都表示子进程的退出码,而这个退出码一般是return的返回值或者exit的参数;正常退出时,status的低8个比特位为全0;而异常退出时,其第8个比特位则为core dump标志位,用来标志是否会有core dump文件产生,而低7个比特位则是退出信号。

    我们可以分别通过以位运算的方式来分别得到以上信息:

    退出码:(status >> 8) & 0xFF

    低7位(检测子进程是否异常退出):status & 0x7F

    • 结果为0则表示正常退出
    • 不为0则说明是异常退出,因为有终止信号

    core dump标志位:(status >> 7) & 0x1

    • 结果为0则表示没有core dump产生
    • 等于1则说明有core dump产生

    通过代码来进一步验证以上结论:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main()
    {
      pid_t pid = fork();
      if(pid < 0) {
        perror("fork");
        return -1;
      }
      else if(pid == 0) {
        //子进程
        printf("I am child, pid is %p\n", getpid());
        sleep(3);
        exit(20); //退出子进程并将其退出码设置为20
      }
      else {
        printf("I am father, pid is %p\n", getpid());
        int status; //定义status,让操作系统为其赋值
        waitpid(-1, &status, 0); //这种传参方式的waitpid和wait几乎没有区别
        printf("进程等待成功!\n");
        //低7位为全0则表示正常退出
        if((status & 0x7F) == 0) {
          printf("正常退出!\n");
          printf("exitcode = %d\n", (status >> 8) & 0xFF);
        }
        else {
          printf("异常提出!\n");
          printf("core dump flag = %d\n", (status >> 7) & 0x1);
        }
      }
      return 0;
    }
    

    运行结果:
    在这里插入图片描述

    4. 进程程序替换

    原理:进程程序替换其实是替换当前正在运行程序的代码段和数据段,并更新堆栈信息。

    有关进程虚拟地址空间以及根据页表映射至物理内存这一模式大家都已经非常熟悉了,这里就不再画图解释。我们需要知道的是,进程程序替换与fork不同,它并不会创建新的进程,而是该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。替换前后的进程号并未改变。

    4.1 exec函数簇

    我们一般通过替换函数来完成进程程序替换,也就是exec函数簇,需要注意的是,它并不是一个函数,而是多个函数。他们都以exec开头,统称exec函数,其函数原型如下:

    #include <unistd.h>
    
    int execl(const char *path, const char *arg, ...);
    int execlp(const char *file, const char *arg, ...);
    int execle(const char *path, const char *arg,..., char * const envp[]);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execvpe(const char *file, char *const argv[],char *const envp[]);
    

    首先说返回值

    • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
    • 如果调用出错则返回-1
    • 所以exec函数只有出错的返回值而没有成功的返回值

    参数解释

    path/file:要替换的可执行程序的名称,path需要带路径
    arg/argv[]:可执行程序的参数,规定其第一个参数必须是可执行程序的名称,并以NULL结尾表示参数传递完毕,二者的区别在于参数是以可变参数列表还是字符数组的方式给出
    envp[]:程序员自己组织的环境变量,以数组的方式给出,内部同样需要以NULL结尾,如果传入NULL则认为当前程序没有环境变量

    如果以上描述还不是很好理解,那么我们可以再仔细观察下这些函数的区别,可以发现,除了开头都是exec这一共同点之外,其余字母无非就是l或v的区别、有没有p的区别以及有没有e的区别:

    l或v的区别

    • l表示命令行参数为可变参数列表,传参数时需要以NULL结尾
    • v表示命令行参数为指针数组,由程序员自己提供

    有没有p的区别:是否会去搜索环境变量

    • 有p则代表会去环境变量PATH去搜索当前要替换程序所在的位置
    • 没有p则意味着不会去搜索环境变量,需要程序员自己提供想要替换的程序所在的路径

    有没有e的区别:是否需要程序员自己组织环境变量

    • 没有e则表示不需要程序自己组织环境变量,内核会将当前的环境变量继承下来
    • 有e则代表需要程序员自己组织环境变量,如果直接传递NULL则表示当前程序没有环境变量;如果自己组织环境变量,则指针数组中得到环境变量需要以NULL结尾

    代码实战:

    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    {
        printf("下面进行进程替换!\n");
    
        //将当前程序替换为ls程序
        execl("/usr/bin/ls","ls","-l",NULL); //l表示命令行以可变参数列表的形式给出,没有p则说明需要带路径,没有e说明不需要自己组织环境变量
    
        //如果进程替换成功,下面的代码将不再执行
        printf("继承替换失败!\n");
    
        return 0;
    }
    

    在这里插入图片描述
    上述代码以execl为例简单的演示了进程程序替换的实际效果,也完全符合我们的预期,其他函数大家可以自己尝试,都非常的简单。

    还需要补充的一点就是:如果使用man去查看这些函数,会发现他们都在3号手册,也就是库函数所在的手册,意味着上述的exec函数簇其实本身都是库函数,而非系统调用。其实,不论是哪个函数,它们最终都会去调用一个叫做execve的系统调用函数,从而真正完成进程程序替换。

    #include <unistd.h>
    int execve(const char *filename, char *const argv[], char *const envp[]);
    

    不仅如此,而这些函数内部,其实也是相互调用的逻辑,不过最终都还是会去调用execve来完成进程程序替换:
    在这里插入图片描述
    文章到这里就结束了,如果感觉博主写的还行的话,就点个赞吧~

    展开全文
  • 进程信息被放在一进程控制块的数据结构中,这个进程控制块称为PCB,进程控制块的数据结构叫task_struct  这个task_struct 包括以下几个信息: 标识符(pid):描述本进程的唯一标识符,用来区别其他进程(学号)...
  • 程序进程,线程的区别和联系

    万次阅读 多人点赞 2018-08-26 22:27:18
    进程程序区别和联系表现在以下方面: 1)程序只是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态的实体。而进程则不同,它是程序在某个数据集上的执行。进程是一个动态的实体,它有自己的生命周期...
  • 程序进程 程序:是指编译好的二进制文件,存放在磁盘上,并不占用系统资源(系统资源包括CPU、内存、打开的文件、设备、锁…) 进程:是一个抽象的概念,与操作系统原理密切相关。进程是活跃的程序,占用系统资源...
  • Linux中的程序进程,PID和PPID

    万次阅读 多人点赞 2015-09-20 15:00:19
    2、进程进程是一个正在执行的程序的实例。 3、进程是动态的。 4、一旦程序被操作系统加载进内存了,那么这程序就成为了进程。 PID和PPID: 1、PID(process ID): PID是程序被操作系统加载到内存成为...
  • 程序进程和线程的区别与联系

    千次阅读 2018-05-18 03:01:21
    程序程序实现特定目标或解决特定问题而用计算机语言编写的命令序列的有序集合。 程序(这里和前边指的是包含了线程,进程程序的抽象概念)有顺序执行(顺序性,只有前一操作结束后才能执行后续操作;封闭...
  • 进程 线程 程序

    千次阅读 2012-07-17 17:16:59
    就相当于将应用程序装进容器里了,你可以往容器里加其他东西(如:应用程序在运行时所需的变量数据、需要引用的DLL文件等),当应用程序被运行两次时,容器里的东西并不会被倒掉,系统会找一个新的进程容器来容纳它。...
  • 程序进程)在cpu中的执行过程

    万次阅读 多人点赞 2018-09-30 10:59:10
    1.可执行程序相关数据代码等加载到内存的相应段? 2.等待cpu调度到此程序,也即是获取cpu的使用权 3.运行balabala... 那我fork一个进程又是什么回事??? 复制与当前程序进程)一模一样的资源与代码??...
  • 程序进程和线程的关系

    千次阅读 2015-12-16 16:20:05
    程序并不能单独运行,只有将程序装载到内存中,系统它分配资源才能运行,而这种执行的「程序」就称之为进程。线程是系统分配处理器时间资源的基本单元。每启动一个程序,就至少启动了一个进程。     进程...
  • 进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程程序是含有指令和数据的...
  • 而线程有时也被称为轻量级进程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程的内存空间(例如打开的文件),但是各个线程拥有自己的栈空间,进程与线程的对比关系如下图。 ...
  • 程序进程与线程(一)

    千次阅读 多人点赞 2018-10-19 09:33:53
    一、程序进程 举个具体的场景,某一天你爸妈不在家,你必须要自己做饭、洗衣服等等。你要做饭首先你得有做饭手册呀,这个手册中包含了做饭所需的各种步骤(比如洗米,米放到电饭煲中,插上电饭煲电源等等),当...
  • 进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程程序
  • 进程,线程,程序的区别和联系

    千次阅读 2013-10-11 15:43:01
    进程程序区别和联系表现在以下方面: 1)程序只是一组指令的有序集合,它本身没有任何运行的含义,它只是 一个静态的实体。而进程则不同,它是程序在某个数据集上的执行。 进程是一个动态的实体,它有自己的生命...
  • 进程和线程的区别(超详细)

    万次阅读 多人点赞 2019-10-03 21:57:46
    进程和线程 进程 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如...与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟...
  • 1.理解进程的概念,明确进程程序的区别 2.理解并发执行的实质。 3.掌握进程的创建、睡民、撤销等进程控制方法。 实验内容 用C语言编写程序,模拟实现创建新的进程:查看运行进程;换出某个进程:杀死运行进程等...
  • 什么进程

    千次阅读 2015-08-19 19:18:16
    进程程序的区别在于:进程是动态的,程序是静态的,进程是运行中的程序,而程序是一些保存在硬盘上的可执行代码。 在Linux下面,可以通过命令ps或pstree查看当前系统中的进程。   好,有了进程的笼统概
  • 进程,线程,程序的理解以及区别

    千次阅读 2012-11-25 15:28:56
    1.什么进程(Process)和线程(Thread)?有何区别?   线程是指进程内的一个执行单元,也是进程内的可调度实体. 与进程的区别: (1)地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址...
  • 进程与应用程序的区别

    万次阅读 2012-04-14 16:29:47
    以Unix操作系统例,进程是Unix操作系统环境中的基本成分、是系统资源分配的基本单位。Unix操作系统中完成的几乎所有用户管理和资源分配等工作都是通过操作系统对应用程序进程的控制来实现的。 C、C++、Java等语言...
  • 在本文中,我们将探讨几种使用gedit作为示例应用程序从命令行以及图形界面终止进程或应用程序的方法。 使用命令行/终止符 Ctrl + C 从命令行调用gedit一个问题(如果您未使用gedit & )是它不会释放提示...
  • 什么进程

    万次阅读 多人点赞 2018-03-25 12:10:57
    程序:完成特定任务的一系列指令集合   代码段+数据段  -----放在磁盘中的程序  进程:进行就是正在进行中的程序  1、用户角度: 进程程序的一次动态执行...又为什么要有进程? CPU一次只能处理一个程...
  • 进程与线程

    万次阅读 多人点赞 2021-03-17 22:50:20
    进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的. 并发性 多个进程...
  • daemon进程称为守护 进程,是在系统 启动就运行,系统关闭才停止的进程,独立于终端之外,不与客户端交互。一般进程在关闭终端后就停止了,而daemon进程不会停止。 怎么判断一个进程是否daemon进程? 因为dae....
  • Java多线程1—程序进程、线程比较

    千次阅读 2011-11-02 09:31:29
    许多人对于程序进程、线程这几个概念许多人都分的不是很清楚,下面我们就简单的介绍一下它们的区别。 程序是计算机指令的集合,它以文件的形式存储在磁盘上。程序是通常我们所写好的存储于计算机上没有执行的...
  • 2. 后台进程:切后台进程称为job,[4] 是job ID , 5094是PID,  1)jobs -l / ps 可以查看后台进程 2)后台进程是依赖控制台的,控制台关闭,相关的后台进程关闭 3)fg 1将job ID 1的进程调到前台来
  • 经常使用 top 命令了解进程信息,其中包括内存方面的信息。命令top帮助文档是这么解释各个字段的。 VIRT , Virtual Image (kb) RES, Resident size (kb) SHR, Shared Mem size (kb) %MEM, Memory usage(kb) ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 367,779
精华内容 147,111
关键字:

为什么把程序称为进程