进程控制

一.进程标识

      Linux中用进程标识符process ID(PID)来唯一的标识一个进程或轻量级进程(Linux中没有真正的线程,而是通过线程组或可以称为轻量级进程组实现的多线程进程)。PID被存放在进程描述符的pid字段中,还有一个字段较tgid用于标识一个进程组,即该组中的所有轻量级进程的线程组ID相同,其值等于领头轻量级进程的PID。我们常用的getpid函数返回的便是进程的tgid,而非pid。若想获取pid可以通过系统调用实现:::syscall(SYS_gettid)进行获取。我们还可以通过一些函数获取进程的实际与有效ID及父进程ID。

      进程0(交换进程)和进程1(init进程,所有进程的祖先,也是所有孤儿进程的父进程)。

 

二.子进程的创建

1.fork(共享与写时复制)

       fork函数用于创建一个子进程,它会在父进程与子进程中分别返回一次,父进程中返回子进程的进程ID,即tgid,当然此时也等于pid(父进程若想知道有哪些子进程,则应该在此时存储,因为没有一个函数可以或得其所有子进程的进程ID)。

      调用fork产生的子进程会与父进程会共享正文段(代码段),而数据段(初始化数据段,未初始化数据段),堆,栈,环境变量表都是不共享的。对于不共享区域不会立刻为二者拷贝一个副本,而是使用了写时复制,即先将这些不共享区域设为只读,当父进程或子进程中的任一个试图修改某一区域时,内核只为修改区域的那块内存拷贝一个副本,一般拷贝副本内存的大小为1“页”(2^K大小的内存块),即拷贝更改区域所在页。

【注】:clone函数是fork的扩充形式,虽然也是创建子进程,但可以控制哪块部分由父进程和子进程共享,比如是否共享打开文件表,是否共享信号处理程序表,阻塞信号表,挂起信号表等(参深入理解Linux内核p119)。

    fork后父进程与子进程谁先执行是不确定的,取决于内核的调度算法,若要对其先后顺序进行控制需要进行进程间通信(不应该使用sleep来决定进程或线程的先后顺序)。

【注】:sizeof函数是编译期计算大小的

2.fork与标准I/O

      当fork一个进程时,若该进程的标准缓冲中还存在数据,即并没有被冲洗,那么当fork后父或子进程进行冲刷(冲刷会改变这块内存的数据,可以将其看作是一种写(更改)),因此会写时拷贝这块内存,这会导致标准缓冲区中的数据在父子进程中各输出一次。测试代码如下:

#include <iostream>
#include <unistd.h>

int main()
{
    printf("write to stdout ");
    if(fork() == 0) {
        printf("child thread\n"); // 输出为:write to stdout child thread
    }
    else{
        printf("main thread\n");  // 输出为:write to stdout main thread
    }
    return 0;
}

3.fork与文件共享

      当fork出一个子进程后,会复制父进程中所有打开的文件描述符,两者共享一个文件对象。

     接下来说明父进程哪些属性会被子进程继承,哪些不会。

1)会继承的属性

  •     实际用户ID,实际组ID,有效用户ID,有效组ID(参考:《文件访问》)
  •     附属组ID
  •     进程组ID
  •     会话ID(不了解)
  •     控制终端(比如输出终端,因为引用的同一个文件对象)
  •     设置用户ID标志与设置组ID标志(参考:《文件访问》)
  •     信号屏蔽和安排(即共享信号处理程序表,阻塞信号表,挂起信号表)
  •     对任一打开描述符的执行时关闭标志
  •     环境(环境变量表(但环境变量表是进程独立拥有的))
  •     链接共享存储段(即共享内存)
  •     存储映像
  •     资源限制
  •     测试发现会继承进程的优先级nice值
  •     互斥锁,读写锁和条件变量的状态(即锁在父进程中是锁住的,在子进程中也是锁住的。)如下例代码所示:
  • #include <pthread.h>
    #include <mutex>
    #include <unistd.h>
    #include <iostream>
    
    int main(int argc, char *argv[])
    {
        std::mutex mut;
        mut.lock();
        if(fork() > 0) {
            // parent
            std::cout<<"parent process"<<std::endl;
            mut.unlock();
            getchar();
    
        }
        else {
            // child
            mut.lock(); // 此处将造成死锁,若不将此句注释掉则永远无法输出child process
            std::cout<<"child process"<<std::endl;
            mut.unlock();
    
        }
        return 0;
    }
    

     

2)有区别的属性

  •     进程ID(tgid与pid)
  •     子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0(进程时间)
  •     子进程不继承父进程设置的文件锁(关于文件锁参考:《文件访问》
  •     子进程的未处理闹钟被清除(alarm)
  •     子进程的未处理信号集设置为空集

4.vfork为exec而生

       vfork也是用于创建一个进程但是为了执行exic而创建,vfork我们或许可以称之为 virtual(虚拟的)的fork,因为它并不会完全复制父进程地址空间到子进程也不会引用,且在调用exec或exit之前,它在父进程的空间中运行,如果子进程修改了数据、进行函数调用或者没有调用exec或exit就返回,都可能带来未知后果,改的是父进程的数据。而在这点上在fork与exec之间就可以更改自己的属性(因为fork出的子进程进行写时复制,而vfork不复制)。

      vfork可以保证子进程先运行,在子进程调用exec或exit之后父进程才可能被调度(即父进程处于休眠状态),这也代表着在它调用exec或exit之前仍依赖父进程的进一步动作会造成死锁,但fork不可。

5.exec

      调用exec函数时指定的文件路径若含有/则视为包含路径(即路径前缀),否则在环境变量PATH所指定的目录下进行寻找。PATH变量包含了一张目录表,各目录之间用:进行分隔,‘.’与零长前缀都表示当前目录。fexecve函数比其它exec类的函数更加安全,因为它使用的是文件描述符,而非文件路径,可以避免获得了特权的恶意用户在文件验证之后,但在进程执行该文件之前替换可执行文件(TOCTTOU攻击)。

      当执行exec后进程ID并不会改变,且会继承下列属性(与fork进行区别,特别是注意往往在fork之后调用exec,这时候哪些属性不会继承自父进程):

  • 进程ID和父进程ID
  • 实际用户ID和实际组ID(fork也会继承,即与父进程相同)
  • 附属组ID(fork也会继承)
  • 进程组ID(fork也会继承)
  • 会话ID(fork也会继承)
  • 控制终端(fork也会继承)
  • 闹钟尚余留的时间(fork出的子进程会清除父进程的未处理闹钟)
  • 根目录
  • 文件模式创建屏蔽字
  • 文件锁(fork不会继承父进程的文件锁)
  • 进程信号屏蔽字(fork也会继承)
  • 未处理信号(fork将父进程的未处理信号集置空)
  • 资源限制(fork也会继承)
  • nice值(用于调整进程优先级)(fork也会继承)
  • tms_utime、tms_stime、tms_cutime和tms_ustime,即进程时间(fork会将这些时间置为0)

       对打开文件的处理与每个描述符的执行时关闭标志值有关(FD_CLOEXEC),若设置了则关闭该描述符,否则该描述符仍然打开(即仍与父进程指向同一文件对象),可以通过exec的参数传入文件描述符。而有效用户ID与有效组ID根据程序文件是否设置了设置用户ID位和设置组ID位。

       exec函数将原想设置为要捕获的信号都更改为默认动作,其它信号的状态则不变,这是因为信号捕获函数的地址很可能在所执行的新程序文件种已无意义了。而当调用fork时,其子进程继承父进程的信号处理方式,因为子进程在开始时复制了父进程的内存映像,所以信号捕捉函数的地址在子进程中是有意义的。

三.子进程的终止

1.终止状态

       在博文《进程环境》讨论了进程终止分为正常终止与异常终止,并说明了各自的几种情况。在正常终止时进程会有一个退出状态,在最后调用_exit时内核将退出状态转换成终止状态,而在异常终止时内核会产生一个指示异常终止原因的的终止状态。

2.僵死进程(僵死状态)

      内核会为每个终止子进程保存一定量的信息(注意除了0进程的所有进程都是init进程的后代进程,因此所有进程都会保存,只是父进程为init的进程的信息被init进程获取了),这些信息至少包括进程ID,该进程的终止状态以及该进程使用CPU时间总量,终止进程的父进程可以通过wait或waitpid或得这些信息。所有终止了但未被父进程处理(调用wait或waitpid等获取信息)的进程被称为僵死进程(即进程状态未僵死状态),使用ps命令打印出的进程信息或显示僵死进程的状态未Z,将2.2中的代码改成如下这样,运行和使用ps命令便可看到。

int main()
{
    printf("write to stdout ");
    if(fork() == 0) {
        printf("child\n");
        std::cout<<getpid() <<" "<<::syscall(SYS_gettid)<<std::endl;

    }
    else{
        printf("parent\n");
        std::cout<<getpid() <<" "<<::syscall(SYS_gettid)<<std::endl;
        getchar();          // 使主线程阻塞在该函数上

    }
    return 0;
}

              

      若父进程在子进程之前终结,那么遵顼以下规则:对于父进程已经终止的所有进程,它们的父进程都改变未init进程。将上面代码改成如下这样,再先后使用ps a -f命令和ps -f -p 子进程ID 命令便可观察到这种变化情况。代码与变化情况如下:

【注】:即使是某个进程的子进程的子进程,只要是该进程的子进程结束了,则该进程的子进程的子进程的父进程也会改变为init,即,当某个进程的父进程终止后,即使该进程还有祖父进程,仍会将父进程变为init

int main()
{
    printf("write to stdout ");
    if(fork() == 0) {
        printf("child thread\n");
        std::cout<<getpid() <<" "<<::syscall(SYS_gettid)<<std::endl;
        while(1){}

    }
    else{
        printf("main thread\n");
        std::cout<<getpid() <<" "<<::syscall(SYS_gettid)<<std::endl;
        sleep(5);
    }
    return 0;
}

在5s内使用ps a -f查看如下:

              

当主进程结束后使用ps -f -p 23878查看结果如下:

              

3.wait系列函数

       当一个进程正常或异常终止时,内核便会向其父进程发送SIGCHLD信号,进程处理该信号的默认行为是忽略,当然也可以提供以一个信号处理函数。

     wait函数用于等待子进程终止,但会使调用者阻塞,waitpid提供了非阻塞选项,且waitpid可以等待指定的进程终止。这两个函数都通过一个int返回进程的终止状态,int中的不同为代表了不同的信息,可以通过APUE p191页的宏进行分析,若是异常终止黑可以用WTFRMSIG宏获取使子进程终止的信号编号。

   waitid提供了更高的灵活性,可以等待某个进程组中的进程,并提供了更多的选项。

    wait3与wait4允许内核返回有终止进程及其所有子进程使用的资源概况。Linux中支持上述5种函数。

 

四.更改用户ID与组ID

       我们可以通过一些函数更改进程的用户ID与组ID(包括有效,实际,保存的设置ID)。所谓保存的设置用户ID或保存的设置组ID是当程序文件设置了设置用户ID位时会将文件的所有用户ID赋予进程的有效用户ID,并保存在保存的设置用户ID中,以便于用户调用函数将有效用户ID在实际用户ID与保存的设置用户ID(即文件的所属ID)之间进行切换。即普通用户可以将进程的有效用户ID设置为实际用户ID或保存的设置用户ID。组ID同理。

1.setuid(uid_t uid)

      当超级用户调用该函数时,会将进程的有效用户ID,实际用户ID,与保存的设置ID都设为uid,若超级用户只想改变有效用户ID则可以使用函数seteuid。但普通用户调用该函数只能设置有效用户ID,且参数uid只可以等于实际用户ID或保存的设置用户ID。

2.setreuid(uid_t ruid, uid_t euid)

     该函数可以交换实际用户ID与有效用户ID,但应该注意换回,不然实际用户ID就有可能具有特权。

 

五.在程序中启动shell

     使用system函数允许我们在程序中启动一个shell程序,system函数会先调用fork在调用fork启动一个shell程序,接着调用waitpid等待该子进程结束。具体参APUE p211。

 

六.进程会计

    当启用该选项后,每个进程结束时内核都会写一个会计记录,一般包括命令名,所使用的CPU时间总量,用户ID和组ID。由于不同系统的实现差别很大,此处没有细看,待以后补回

七.进程调度

      进程通过调整nice值调整进程的优先级,nice值越低,进程优先级越高(进程越友好,可以忍耐的就越多,也就优先级越低)。只有root权限的用户可以降低nice的值(即调高优先级)。

      Linux下进程的默认nice值为0,nice的取值范围为-20 ~ 19,可参《深入理解linux内核》 p263。Linux的调度算法在《深入理解linux内核》笔记中进行说明。

      由上可知nice返回-1时可能是合法值,因此在调用nice前应该先保存errno的现有值,再将errno清空,调用nice后若返回-1则需进一步检查errno是否由错误。

     getpriority函数除了获取可用来取得进程、进程组中进程和用户创建的进程的执行优先权,若果有多个进程,如进程组,则返回优先级最高的那个。

int main()
{
    int prio = nice(20);
    int userPrio = getpriority(PRIO_USER,0);
    std::cout<<"userPrio = "<< userPrio<<std::endl; // 调用用户创建进程的默认优先级
    std::cout<<"nice = "<< nice(0)<<std::endl;
    if(fork() == 0) {
        printf("child thread\n");
        std::cout<<getpid() <<" "<<::syscall(SYS_gettid)<<std::endl;
        std::cout<<"nice = "<< nice(0)<<std::endl;
    }
    else{
        printf("main thread\n");
        std::cout<<getpid() <<" "<<::syscall(SYS_gettid)<<std::endl;
        std::cout<<"nice = "<< nice(0)<<std::endl;
    }
    return 0;
}

八.进程时间

      待补

      

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值