• UNIX环境高级编程--8. 进程控制


    进程控制
    进程标识:
        每一个进程都有一个非负整型表示的唯一进程ID。虽然唯一,但是ID可以复用。当一个进程结束后,其进程ID会被延迟复用。
        ID=0的进程通常是调度进程,常被称作交换进程(swapper)。改进程是内核的一部分,它不执行任何磁盘上的程序,因此也被成为系统进程。
        ID=1的进程通常是init进程,在自举过程结束时由内核调用。该进程负责在自举内核后启动一个UNIX系统。init进程通常读取与系统有关的初始化文件,并将系统引导到一个状态(如多用户状态)。init进程绝不会终止。它是一个普通的进程(ID0是系统进程),但是以root用户特权运行。会成为所有孤儿进程的父进程。
        ID=2的进程是页守护进程,负责支持虚拟存储器系统的分页操作。
        getpid()     返回进程ID
        getppid()     返回父进程ID
        getuid()    返回进程的实际用户ID
        geteuid()    返回进程的有效用户ID
        getgid()    返回进程的实际组ID
        getegid()    返回进程的有效组ID

    函数fork:
        一个现有的进程可以调用fork函数创建一个新进程。
        pid_t fork(void);
        由fork创建的新进程被称为子进程。调用一次返回两次。子进程返回0,父进程返回值是新建子进程的进程ID。
        例程8-1使用fork函数,可以看出子进程对变量的改变并不影响父进程中的该变量。原因:子进程与父进程只共享正文段(代码段),初始化的数据段,未初始化的数据段(bbs),堆,栈都是不共享地!但是共享文件描述符,文件描述符!文件描述符!
        在执行fork之前只有一个进程执行这段代码,但fork之后变成了两个进程在执行这段代码。开始执行的语句为 fork之后的语句。fork之后先执行父进程还是子进程是不确定的,一般由操作系统进程调度算法决定。如需要信息同步,信号一种方法。
        解释一下为什么输出定向到文件的时候会多打印一行“write to stdout”。最根本的原因是,输出流链接到设备时(比如终端)是行缓冲的(遇到' '冲洗缓冲区),当输出流重定向到一个文件时是全缓冲的(结束时一起冲洗)。所以当行缓冲时,fork之前直接输出到终端,子进程复制父进程的存储空间时(当然除了正文段,剩下的数据段、bbs、堆、栈都是要自己开辟的)就没能复制到缓存空间中的这行文字,但是当重定向到文件时,就会复制到子进程的缓存中,最后遇到子进程中的(exit之前点 )printf函数,将这条“write to stdout”与带输出的信息一并输出了。所以比终端下输出会多一行信息。
        这个挺重要的:
        fork函数的两种用法:
        (1)一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的———父进程等待客户端的请求。当请求到达时,父进程调用fork,让子进程去处理请求,而父进程继续等待下一个请求。
        (2)一个进程要执行一个不同的程序。这对shell比较常见。在这种情况下,子进程从fork返回后立即调用exec。这种fork后立即执行exec的组合操作在一些OS中被组合成一个操作,spawn。

    pthread和fork
        pthread创建的是线程,fork创建的是子进程
        
    文件共享:
        再重定向父进程的标准输出时,子进程的标准输出也会被重定向。fork将父进程的所有打开文件描述符都复制到子进程中。子进程与父进程每个相同的文件描述符共享一个文件表项。

    函数vfork:
        vfork函数与fork函数调用序列、返回值一致。
        vfork函数用于创建一个新进程,而该进程的目的是exec一个新程序。
        fork后得到一个复制了父进程地址空间的函数,执行和父进程一样的代码,使用vfork后得到一个新的进程然后调用exec将进程ID赋给新的可执行程序。

    fork与vfork的区别:
        (1)vfork不会将父进程的地址空间完全复制到子进程。而是在执行exit或者exec之前,它还是在父进程的地址空间中执行。
        (2)vfork保证子进程先运行,在调用exec或exit之后父进程才可能被调度运行。(如果调用exec或者exit之前,子进程依赖于父进程那么将会导致死锁)
        例程:通过输出pid可以看到当fork函数返回0的时候,即已经进入子进程。得到的进程ID已经=父进程+1了。并且vfork一定会先执行子进程。子进程+1改变了父进程的操作,结果改变了父进程的变量值。因为子进程在父进程的地址空间中运行。

    函数exit:
        进程有5种正常终止以及3中异常终止的方式。5种正常终止的方式:
        (1)在main函数内执行return语句。等效于调用exit。
        (2)调用exit函数。其操作包括调用各终止处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准I/O流等。因为ISO C并不处理文件描述符,多进程以及作业控制,所以这一定以对UNIX兄台哪个而言是不完整的。
        (3)调用_exit或_Exit函数。
        (4)进程的最后一个线程在其启动例程中执行return语句。
        (5)进程的最后一个线程调用pthread_exit函数。
        3种异常终止:
        (1)调用abort。它产生SIGABRT信号,是一种异常终止的特例
        (2)当进程接收到某些信号时。
        (3)最后一个进程对“取消”请求做出响应。
        不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。

    退出状态与终止状态:
        是有区别的。调用_exit函数,内核将退出状态转换成终止状态。
        如果父进程在子进程之前终止,那么父进程已经终止的所有进程他们的父进程都会改变为init进程。这些进程被init进程收养。
        当终止进程的父进程调用wait或waitpid时,可以的得到这些信息。

    僵死进程:
        UNIX术语里,一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被成为僵死进程(zombie)。如果编写一个长期运行的程序,它fork了很多子进程,那么除非父进程等待取得子进程的终止状态,不然这些子进程终止后就会变成僵死进程。

    函数wait、waitpid:
        当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是一个异步事件(可以在其父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用的函数(信号处理程序)。这种信号的系统默认动作是忽略。
        调用wait和waitpid函数会发生:
        (1)如果其所有子进程都还在运行,则阻塞。
        (2)如果一个子进程已经终止,正等待父进程获取其终止状态,则获取该子进程的终止状态立即返回。
        (3)如果他没有任何子进程,立即出错返回。
        ------------------------------------莫名出现的分割线-----------------------------------------------------
        两个函数的区别:
        (1)一个子进程终止前,wait使其调用者阻塞。而waitpid有一个选项,可使调用者不阻塞。
        (2)waitpid并不等待在其调用之后的第一个终止子进程,他有若干选项,可以控制他所等待的进程。
        wait函数只要又一个线程终止就返回,如果要等待一个特定的进程就需要使用waitpid函数。
        waitpid有一个pid参数:
        pid == -1 等待任意一子进程,等效于wait函数
        pid > 0    等待进程ID = pid的子进程
        pid == 0 等待组ID等于调用进程组ID的任一子进程
        pid < -1 等待组ID等于pid绝对值的任一子进程
        waitpid提供了wait没有提供的3个功能:
        (1)waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。
        (2)waitpid提供了一个wait的非阻塞版本。有时候希望获得一个子进程的状态,但不想阻塞
        (3)waitpid通过WUNTRACED和WCONTINUED选项支持作业控制。

    函数waitid:
        与waitpid相似,waitid允许一个进程指定要等待的子进程。使用两个单独的参数表示要等待的子进程所属的类型那,而不是将此与进程ID或进程组ID组合成一个参数。
        
    函数wait3和wait4:
        比wait waitpid waitid多了一个参数,该参数允许内核返回由终止进程以及所有子进程使用的资源概况。包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。
            
    竞争条件:
        在上面的8-8例程中,我们调用sleep(2)函数,使得child1线程也就是child2线程的父进程提前执行到exit那2的父进程就变成了init进程。如果在一个负荷较重的系统中,有可能在进程child2醒来之后child1还没得到执行机会,使得线程child2还是在child1之前执行,那么打印得到的线程id将会是线程child1而不是init。
        如果一个进程希望等待一个子进程终止,则他必须调用wait函数中的一个。如果一个进程要等待其父进程终止,则可使用下列形式的循环:
        while(getppid() != 1)
            sleep(1);
        这种形式叫做 轮询(polling),他的问题是浪费了CPU时间。为了避免这种轮询机制,多个进程之间需要使用某种形式的信号————信号机制。
        这样一种情况:在fork之后父进程和子进程都有一些事情要做。比如,父进程可能要用子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。这样两个进程之间的某些操作要等待另一个进程的进展,为了满足这种形式可能需要如下形式:
        #include "apue.h"
        TELL_WAIT();    /*set things up for TELL_xxx & WAIT_xxx*/
        if ((pid = fork()) < 0)
            err_sys("fork error");
        else if(pid == 0){    /* child */
            /* child does whatever is necessary...*/
            TELL_PARENT(getppid());    /*tell parent we're done*/
            WAIT_PARENT();    /* and wait for parent */
            /* and the child continues on its way... */
            exit(0);
        }
        /* parent does whatever is necessary...*/
        TELL_CHILD(pid);    /* tell child we're done */
        WAIT_CHILD();    /*and wait for child */
        /* and the parent continues on its way...*/
        exit(0);
        假定在头文件apue.h中定义了需要使用的各个变量。5个例程TELLWAIT、TELL PARENT、TELL_CHILD、WAIT_PRAENT以及WAIT_CHILD可以是宏,也可以是函数。TELL WAIT 可以使用信号实现、管道实现。
        例程8-12先使用了不带任何竞争控制的方法,看到两个进程交替输出字符。
        然后使用能够TELL和WAIT函数实现8-13的程序。

    函数exec:
        用fork创建子进程,子进程需要调用exec函数以执行另一个程序(子进程中也需要做一些复杂的操作,而不仅仅是从父进程那里继承下来的变量++--操作而已)。当进程调用exec函数,子进程执行的程序完全换成为新程序,而新程序从其main函数开始执行。因为调用exec函数并不创建新进程,所以前后的进程ID不改变。exec只是就用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
        一共有7个小exec函数可供使用。
        用fork创建新进程,用exec可以初始执行新的程序。exit函数和wait函数处理终止和等待终止。这些是我们需要的基本的进程控制原语。
        内存空间:正文段(代码段)、数据段(初始化数据段、为初始化数据段bbs)、堆段、栈段 组成了进程的内存地址空间。fork就可以创建新的子进程,拥有自己的地址空间,内容是复制的父进程的地址空间上的内容,共享父进程的文件描述符。exec可以在子进程中执行新的程序,exit函数用来返回当前进程的结束状态给父进程。wait用来在父进程中控制子进程。
        1、execl
        2、execv
        3、execle
        4、execve
        5、execlp
        6、execvp
        7、fexecve    :    依赖调用进程来寻找正确的可执行文件。避免可执行文件被恶意替换。
        1-4使用路径名作为参数,5-6文件名作为参数,7使用文件描述符作为参数
        环境变量:
        PATH=/bin:/usr/bin:/usr/local/bin:
        

    更改用户ID和更改组ID:
        函数setreuid和setregid:交换实际用户ID和有效用户ID的值
        函数seteuid和setegid:类似于setuid和setgid,但只更改有效用户ID和有效组ID
        组ID:上述方法都适用于组ID。

    解释器文件:
        UNIX系统都支持解释器文件(Interpret file)。这中文本文件,其起始行形式是: #! pathname [optional-argument]
        在!和pathname之间的空格是可选的。最常见的解释器文件以下列行开始:
        #! /bin/sh
        pathname 一般都是以/开头的绝对路径。
        程序中用execl调用解释器文件,解释器文件描述了调用那些解释文件,用bash,解释文件执行特定的功能,一般都是可执行文件。
        好处:
            (1)有些程序使用某种语言写的脚本,解释器文件可将这个事实隐藏起来。比如,只需要使用下列命令行:
                awkexample optional-arguments
            否则:
                awk -f awkexample optional-arguments
            (2)解释器脚本效率高。如果将程序放在一个shell脚本中:
            awk 'BEGIN{
                for(i = 0; i < ARGC; i ++)
                    printf "ARGV[%d] = %s ",i , ARGV[i]
                exit
            }' $*
                这种办法的问题是要求做更多工作。首先,shell读次命令,然后试图execlp次文件名。因为shell脚本是一个可执行文件,
                但却不是机器可执行的,于是返回一个错误,execlp才明白文件是一个shell脚本。然后执行/bin/sh,并以shell脚本的路
                径名作为参数,shell正确的执行我们的shell脚本,但是为了awk程序,它调用fork、exec、wait。于是会造成很多开销。
            (3)解释器脚本使我们可以使用除/bin/sh以外的其他shell来编写shell脚本。

    函数system:
        用来在程序中执行一个命令字符串。
        比如:system("date");  等效于 在命令行中 date命令
        这里可以是任何的命令,当然也包括自己写的可执行程序(命令)。
        好处是system进行了所需要的各种出错处理以及信号处理。

    用户标识:
        任何一个进程都可以得到其实际用户ID和有效用户ID以及组ID。但是有时候我们希望得到运行该程序用户的登录名。
        
    进程调度:
        调度策略和调度优先级是由内核确定的。进程可以通过调整nice值选择以更低优先级运行(通过降低nice值来降低它对CPU的占有,因此该进程是“友好的”)。
        nice值越低,优先级越高。可以通过nice函数来获取nice值。    
        
    进程时间:
        时钟时间(墙上时钟时间wall clock time):从进程从开始运行到结束,时钟走过的时间,这其中包含了进程在阻塞和等待状态的时间。
        用户CPU时间:就是用户的进程获得了CPU资源以后,在用户态执行的时间。
        系统CPU时间:用户进程获得了CPU资源以后,在内核态的执行时间。
        用户态:程序运行在0级(最高特权级)
        内核态:程序运行在3级(最低特权级)
        转换:当用户态产生 (1)系统调用,会进入内核态; (2)产生异常,调用内核的异常处理程序;(3)外围设备中断,暂停用户态执行转而调用系统中断服务程序。
        
    每天学一点Linux命令:
    1. 在gcc编译源文件时,可能会因为一些警告导致源程序无法编译下去(比如:除0),可以在命令加入+ -w 来消除所有警告 -W是显示所有警告

    2. find .|xargs grep -r "TELL_WAIT" -l   我希望在当前目录下的所有文件中找到包含“TELL_WAIT”的文件 ; 命令详解:
        find命令:find /etc -name "*.log"  从/etc下查找“.log”文件
        .  (|前面的.)代表搜索的文件路径
        |xargs代表从输入中构建和执行shell命令,参数是|前面的 find . 意思是在当前路径下查找
        grep命令:使用正则表达是查找文本
        -r 当前目录以及其子目录中
        -l 只显示文件名,不+会产生更详细的信息
       find /usr/share/man -type f -print | xargs grep getrlimit    在路径下 找到包含getrlimit的文件, -type f限定输出列表只包含普通文件。

        管道和xargs区别:
            管道是实现“将前面的标准输出作为后面的标准输入”
            xargs是实现“将标准输入作为命令的参数”
            你可以试试运行:
            代码:
            echo "--help"|cat
            echo "--help"|xargs cat
            看看结果的不同。
       

  • 相关阅读:
    redhat 6.4重新安装python和yum
    【机器学习】置信区间上界算法UCB(Upper Confidence Bound)
    MAC终端zsh配置
    [Android] 重新打包(替换)签名APK
    UNI-APP常用方法
    一个请求的生命周期
    jquery追加元素的几种方法?(包括append()、prepend()、after()、before()、insertAfter()、insertBefore())
    怎样查外键建在哪个表上
    Redis实现分布式锁
    sql语句中对单个字段去重,distinct和group by性能分析
  • 原文地址:https://www.cnblogs.com/luntai/p/6164218.html
Copyright © 2020-2023  润新知