Linux高级环境编程之7执行单元_进程管理

课程目标

(1) 掌握进程的基本概念,进程属性获取。
(2) 掌握进程的生命周期以及资源申请与释放的过程。
(3) 掌握创建新进程时父子进程资源的管理。
(4) 掌握守候进程等特殊进程的管理。
(5)多进程在编程中的应用。

主要知识点

(1) 程序,进程,进程资源。
(2) 进程的生命周期,进程状态。
(3) 进程创建与父子进程资源。
(4)进程属性管理与进程应用。

1. 程序、进程、进程属性与进程状态

  • 进程是unix/Linux中基本的资源管理单元。
  • 进程又是执行的代码片段(一个进程可能创建多个进程,一个进程可以执行多个程序的代码)。将一个代码片段加载到内存并让其执行也就是创建一个(或多个)进程。
  • 进程与程序的关系:程序存储在磁盘上,是一个文件;而进程是一个加载到内存执行的程序段,且有生命周期,创建、执行、退出、等待的状态
  • 一个进程不仅仅占用了加载代码的内存(用户空间),在Linux下,使用task_struct这个结构体来维护整个进程的资源(内核空间)
  • 在内核中task_struct完整的描述了一个进程的所有信息:
volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */ 描述了状态信息
struct mm_struct *mm, *active_mm; 与内存存储相关信息,描述了在用户空间中的代码段,数据段,堆, 栈, mmap等涉及的所有内存信息
/* open file information */
struct files_struct *files;  打开文件的信息列表,成员fd_array[NR_OPEN_DEFAULT]就是描述了这个进程打开的文件列表。
/* signal handlers */
struct signal_struct *signal;  信号的描述信息
struct sighand_struct *sighand; 信号的描述信息
  • 应用编程时产看进程ps aux
  • 用户空间的进程属性:
    PID:进程号,每个进程有唯一的编号
    状态:如下描述

Linux系统是一个多任务多用户的系统。为什么会用多进程,意义何在?
现在CPU的速度非常快,而人的反映时间时微秒级,系统的外设的速度的速度也相对比较慢。如果使用单进程任务系统,则CPU有大量空闲。因此,我们让CPU在各个执行单元中不停的调转。当某个进程需要等待其他的资源时,CPU转而执行其它的进程,充分利用了CPU资源。在什么时候执行哪一个进程实质是由调度算法来决定的。
CPU只有一个,而进程有多个,因此某个时刻只能执行一个进程,其他的进程则处于相应的其他状态
运行状态:占有资源,执行;
就绪状态:除了CPU资源外,其他资源都已获取,等待调度算法来执行;
等待状态:除了CPU资源外,还需要等待其他资源或时间,分为可中断等待(可以被信号打断)和不可中断等待
停止状态:正在被跟踪或者调试的进程
僵死状态:用户资源已经收回,PCB内存资源没有收回,已经不能执行

怎么来划分多个进程呢?
进程是资源管理的基本单元,创建进程时,一个比较独立的任务(事务)创建一个进程,这个事务尽量不与其他进程有太多的耦合性。

2. 进程管理应用及资源

(1)进程创建:进程创建于进程资源获取。fork/vfork

在进程创建过程中:
用户空间中:将程序的代码段,数据段,BBS段,从磁盘加载到内存,并且申请堆栈空间;
内核空间中:为这个进程分配唯一的PID标识,同时在内核中为这个进程申请进程控制块PCB,初始化相关信息
在运行的过程中,涉及打开的文件,关联的终端,安装信号,状态等系列信息
在创建进程时,由父进程来创建子进程

       #include 

       pid_t fork(void);
在父进程中,返回子进程的ID,在子进程中返回,返回0
(2)进程中执行新的代码

在进程执行过程中,要执行新代码,实际上是创建了一个新进程,更多的是期望在这个进程中执行新的代码,而不是原来的程序代码。使用exec相关的函数可以在子进程中,替换原有进程的代码段和数据段(用户空间的信息),转而执行新代码

       #include 

       extern char **environ;

       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[]);
参数说明:
path:可执行程序的路径
arg:参数列表(以NULL结束)
file:文件名(要求系统在$PATH环境变量所列路径下搜索)
argv[]:执行这个程序的参数列表字符串指针数组

用法参考:
execl("/bin/ls", "ls", "-l", NULL);
execlp("ls", "ls", "-l", NULL);
char *const argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
(3)进程退出

进程在以下几个情况会退出:

  • 使用kill函数 kill -9 pid:强制退出
  • 显示的执行exit系列函数
NAME
       _exit, _Exit - terminate the calling process
SYNOPSIS
       #include 
       void _exit(int status);

       #include 
       void _Exit(int status);

NAME
       exit - cause normal process termination
SYNOPSIS
       #include 
       void exit(int status);

exit()与_exit()的区别:
exit()在退出时会对进程资源做清理,例如刷新流流的缓冲区;
_exit()不做任何处理,直接退出。
  • 进程遇到main函数的return或者遇到}没有代码可执行时退出

exitreturn的区别:
exit是一个系统函数,退出这个进程;
return是C/C++关键字,退出这个函数。

  • 在退出时,我们可以在退出前注册退出时执行的代码
NAME
       atexit - register a function to be called at normal process termination
SYNOPSIS
       #include 
       int atexit(void (*function)(void));

NAME
       on_exit - register a function to be called at normal process termination
SYNOPSIS
       #include 
       int on_exit(void (*function)(int , void *), void *arg);

注册回调函数,即执行exit()或者正常退出时去执行相应的回调函数代码,实际上是提供一个功能,在退出进程时完成相应的进程资源清理工作,相当于C++中的析构。

(4)进程资源回收

进程退出有退出的状态,且进程资源的回收在exit的时候仅仅释放了它的用户空间资源,而内核空间资源PCB没有回收,转而由它的父进程通过wait相关的函数来回收
子进程在退出时会给父亲进程发出一个信号(SIGCHLD),父进程可以显示的调用wait/waitpid等待子进程结束并回收资源,回收资源时,也可以得到子进程退出的状态

NAME
       wait, waitpid, waitid - wait for process to change state
SYNOPSIS
       #include 
       #include 

       pid_t wait(int *status); 
参数status用来存储子进程退出状态,返回值为退出的子进程PID,
这个函数以阻塞的方式等待某个进程退出,当进程退出后,此函数返回。
       pid_t waitpid(pid_t pid, int *status, int options);
此函数为指定等待某个进程或者某些进程,其中第一个参数可以为以下值:
       < -1   meaning wait for any child process whose process group ID is equal to the absolute value of pid.
       -1     meaning wait for any child process.
       0      meaning wait for any child process whose process group ID is equal to that of the calling process.
       > 0    meaning wait for the child whose process ID is equal to the value of pid.
第二个参数status用来存储子进程退出状态
第三个参数options一般为0

使用示例:

#include 
#include 
#include 
#include 
#include 

int main()
{
    pid_t pid = fork();

    if( pid < 0 )
    {
        perror("fork");
    }
    else if( pid == 0 ) 
    {
        sleep(1);
        printf("child ID = %d\n", getpid());
        exit(10);
    }
    else
    {
        int stat;
        pid_t w_pid;

    //  w_pid = wait(&stat);
        w_pid = waitpid(-1, &stat, 0);

        printf("status = %d, wait_pid = %d\n", stat>>8, w_pid);
    }

    return 0;
}

总结资源申请与释放的问题:
创建时,申请进程的所有资源。内核空间中的PCB,用户空间中的加载了代码段,数据段,BSS段,申请了堆栈空间,打开的文件,安装的信号,关联的终端等;
执行时,替代代码段,数据段,BSS段,堆栈空间等用户空间信息,但内核PCB的信号没有修改;
退出时,释放了自己用户空间的资源
回收时,回收内核空间资源。

两个重要的概念:

  • 僵死进程:进程已经退出,但是内核空间资源没有回收的进程
  • 孤儿进程:父进程先于子进程退出,这样的子进程就是孤儿进程,其父进程会被转移到init(pid=1)进程

3. 进程创建详解与父子进程资源

(1)父子执行顺序问题

父子进程在创建完子进程后互不相关联,以独立身份抢占CPU资源,具体谁先执行由调度算法决定,用户空间没办法干预。子进程执行代码的位置是fork/vfork函数返回的位置。

(2)子进程资源申请问题

子进程重新申请新的物理内存空间,复制父进程地址空间所有的信息(现在的操作系统实际采用写时复制等策略,真正的物理内存空间发生在需要写入时)
子进程在用户空间中复制父进程的代码段,数据段,BSS段,堆,栈所有的信息在内核空间中操作系统为其重新申请一个PCB,并且使用父进程的PCB来初始化,除了pid特殊信息外,几乎所有的信息都是一样的

  • 父子进程中资源申请问题
#include 
#include 
#include 
#include 
#include 

int glob = 100;

int main()
{
    pid_t pid = fork();
    int num = 20;

    if( pid < 0 )
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    else if( pid == 0 ) 
    {
        num = 1000;
        glob = 2000;
        printf("child process: num = %d, glob =%d\n", num, glob);       // child process: num = 1000, glob =2000
        printf("child process: &num = %p, &glob =%p\n", &num, &glob);   // child process: &num = 0xbfe94ff8, &glob =0x804a028
    }
    else
    {
        sleep(1);
        printf("parent process: num = %d, glob =%d\n", num, glob);      // parent process: num = 20, glob =100
        printf("parent process: &num = %p, &glob =%p\n", &num, &glob);  // parent process: &num = 0xbfe94ff8, &glob =0x804a028
    }

    return 0;
}

观察输出结果:
在子进程中先修改变量的值,并不影响父进程,明数据段,栈(当然也包括其它用户空间内存),子进程是申请新的物理空间;
但从打印的地址来看,父子进程中的变量地址为同一个地址,这是为什么?
这里打印的是虚拟地址,而不是物理地址编号;两个进程的虚拟地址空间是没有任何联系的。

  • 父子进程中文件流的缓冲区状态
#include 
#include 
#include 
#include 
#include 

int main()
{
    printf("hello\nworld");

    fork();
    printf("bye\n");

    return 0;
}

===============
输出结果:
hello
worldbye
worldbye

流的缓冲区会缓存没有刷新的信息,且缓冲区在用户空间中,虽然子进程创建后从fork返回处执行,但缓冲区被子进程复制了一份,这里存储在缓冲区中的world也被复制了一份,因此,输出了两份world。

#include 
#include 
#include 
#include 
#include 

int main()
{
    for(int i=0; i<2; i++)
    {
        fork();
        printf("*");
    }

    return 0;
}

输出结果:
********
输出过程详解

4.子进程创建或执行execX后,对打开的文件的操作方式

根据前面所学的内容,在同一个进程中,两次打开(使用open)同一个文件(只要没有对文件上锁),分别写入文件会存在覆盖的情况。
而使用fcntl/dup复制文件描述,分别使用这两个文件描述符写文件并不会出现覆盖,而是交叉写入
原因:两次open打开实际上在内核中创建了两个互不相关的文件表项(struct file),也就记录了两个读写位置。而复制文件描述符则在内核中使用同一个文件表项,因此,共用以一个读写位置。

  • 在子进程中是如何操作父进程中打开的文件?
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int fd;
    int fd2;

    fd = open("test.txt", O_CREAT|O_RDWR);

    if( fd < 0 ) 
    {
        perror("open");
    }

    pid_t pid = fork();

    if( pid == -1 )
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    else if(pid == 0)
    {
        write(fd, "helloworld", 10);
    }
    else
    {
        sleep(1);
        write(fd, "abc", 3);
    }

    close(fd);

    return 0;
}

输出结果:
helloworldabc

通过以上代码测试,父子进程共享一个文件表项(file struct),也就是共用一个读写位置。



操作文件时,父子进程共用读写位置。

  • 在execX系列函数替换代码后,对打开的文件能够再处理吗?
    默认情况下,execX执行的代码可以访问在原来代码中打开的文件,操作是同一个文件描述符,即用一个文件对象
    fcntl(fd,F_SETFD,FD_CLOEXEC);
    语句用来使在execX之前打开的文件描述符在新的代码中不可用。

5. 进程属性获取与修改

进程的属性包括进程组属性用户属性

(1)进程组属性决定了进程中运行过程中控制权限以及相关控制信息
  1. PID:进程号。当前进程在当前系统下唯一的编号,针对这个进程执行的操作多以进程号为标识:例如,等待某个子进程结束waitpid;向某个进程发送信号。用户可以获取getpid(),但不能通过函数修改进程号的值。
  2. PPID:父进程号。一般为创建这个进程的那个进程的ID,一般也不会修改,当这些进程的父进程退出后,当前进程变成孤儿进程,它的PPID会被修改成init进程,即PID=1的这个进程。
  3. PGID:进程组号。将完成协同工作的多个进程默认为一个进程组。例如,在终端运行一个新的程序,新的程序创建的子进程以及自身在一个进程组下,而第一个进程默认为进程组长,进程组号也是进程组长的ID。PGID可以被获取和修改,getpgid(pid_t pid):参数为某个进程的ID,返回该进程的进程组长编号;setpgid(pid_t pid, pid_t pgid):修改某个进程的进程组长。

修改一个进程的进程组号的意义:kill可以向一个进程组发起信号,要影响整个这个进程组的所有进程。

  1. SID:会话ID(session ID)。会话:进行交互。一般,在某个终端下执行的程序所创建的进程/进程组,它们的SID就是这个终端的编号。在一个会话下的所有进程都受到这个会话终端的影响。
    getsid(pid):获取某个进程的会话ID。
    setsid():设置某个进程为会话组长,要求这个进程不能说进程组长。一般在创建守候进程时会修改SID,避免原来关联进程的终端信号影响子进程。
  2. 终端
    一个进程可以与某个终端关联,建立与控制终端关联的这个会话首进程为控制进程
    一个会话中的多个进程组可以分为一个前台和多个后台
    在终端下执行键盘命令ctrl+c等,会将信号发送给前台进程组所有进程。
NAME
       tcgetpgrp, tcsetpgrp - get and set terminal foreground process group

SYNOPSIS
       #include 

       pid_t tcgetpgrp(int fd);

       int tcsetpgrp(int fd, pid_t pgrp);
(2) 用户属性决定了进程在运行时对其他资源的访问权限,如对文件的读写权限
  1. uid/ruid:创建这个进程的用户的ID。例如用户ID为500,uid就是为500
  2. gid/rgid:创建这个进程的用户所在组的id。
  3. EUID:有效用户ID,一般同uid。
  4. EGID:有效用户组ID,一般同gid。

一般对文件真正的访问权限由EUID和EGID决定,当EUID和EGID仅仅是在这个可执行程序的setuid位和setgid位被设置时,相应的EUID和EGID将与执行这个进程的UID/GID不同,上升到了这个可执行程序的setuid用户。
如:

delphi@delphi-vm:~/code/linux_coding$ ll /usr/bin/passwd 
-rwsr-xr-x 1 root root 37100 2011-02-15 06:12 /usr/bin/passwd*
passwd的可执行程序setuid被置位,普通用户执行这个程序时,UID是普通用户ID,
但EUID上升到了这个文件的拥有者root,即这个进程对文件的访问权限为root用户的权限。
因此可以修改/etc/passwd这个文件。

你可能感兴趣的:(Linux高级环境编程之7执行单元_进程管理)