zoukankan      html  css  js  c++  java
  • 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
            看看结果的不同。
       

  • 相关阅读:
    【leetcode】1630. Arithmetic Subarrays
    【leetcode】1629. Slowest Key
    【leetcode】1624. Largest Substring Between Two Equal Characters
    【leetcode】1620. Coordinate With Maximum Network Quality
    【leetcode】1619. Mean of Array After Removing Some Elements
    【leetcode】1609. Even Odd Tree
    【leetcode】1608. Special Array With X Elements Greater Than or Equal X
    【leetcode】1603. Design Parking System
    【leetcode】1598. Crawler Log Folder
    Java基础加强总结(三)——代理(Proxy)Java实现Ip代理池
  • 原文地址:https://www.cnblogs.com/luntai/p/6164218.html
Copyright © 2011-2022 走看看