操作系统---进程管理

一、进程介绍

进程与程序:
  • 程序是存储在磁盘上的可执行文件,里面包含可执行的机器指令和数据的静态实体;进程是处于活跃状态的计算机程序,也就是正在运行中的程序

  • 一个运行中的程序,可能由多个进程组成,但至少要有一个进程,称为主进程,同时可以通过系统调用创建出若干个子进程同时进行任务

  • 一个程序也可以同时运行出若干个进程

进程的分类:

根据进程的功能不同一般分为三类:交互进程、批处理进程、守护进程

  • 交互进程:由一个shell终端启动的进程,在执行过程中,需要与用户进行交互操作,可以运行在前台,也可以运行在后台

  • 批处理进程:该进程是一个进程指令集合,负责按顺序去启动其他进程

  • 守护进程:一般都处于活跃状态,运行在后台,由系统在开机时通过脚本自动创建并运行。

进程查看:
简单形式:

ps: 以简略的形式显示出当前用户有控制终端控制的进程信息

复杂形式:

ps auxw 以更宽大的列表形式详细地列出所有用户的进程信息

a - 所有用户的有终端控制的进程

x - 包括无终端控制的进程

u - 以更详细的内容显示

w - 以更大的列宽显示

e - 显示所有进程

f - 显示出其他信息字段

进程的信息列表:
  • USER : 进程属主

  • PID : 进程ID

  • %CPU : CPU使用率

  • %MEM :内存使用率

  • VSZ : 占用虚拟内存大小(Kb)

  • RSS : 占用物理内存大小(Kb)

  • TTY :控制终端设备号 ? 表示无终端控制 ,例如后台进程

  • STAT :进程状态 ,可有以下值:

    • O - 就绪态 ,表示等待被调度

    • R - 运行态,Linux下没有O状态,就绪态也用R表示

    • S - 可被唤醒睡眠态。当系统中断、获得资源、收到信号等都可以被唤醒转入回运行态

    • D - 不可被唤醒睡眠态。只能被wake_up系统调用唤醒

    • T - 暂停态。收到停止类信号转入暂停态,当收到SIGCONT(18)转入运行态

    • Z - 僵尸态。已经停止运行,但是父进程尚未回收相关资源

    • X - 死亡态。不可见

    • N - 低优先级

    • < - 高优先级

    • s - 进程组的领导

    • l - 多线程化的进程

    • + 在前台的进程组中的

    • L - 有被锁入内存的分页

  • START : 进程启动的时间点

  • TIME : 进程运行的耗时时间

  • COMMAND :启动进程的指令

# 查看指定进程  
ps aux | grep bash      #过滤出包含bash关键字的进程信息
​
# 分页查看进程
ps aux | more
​
# 查看指定用户进程
ps -u 用户名 uw
父进程与子进程:
  • 一个进程可以创建出另一个进程,创建者称为被创建者的父进程,被创建者称为创建者的子进程

  • 父进程创建出子进程后,子进程在操作系统的调度下与父进程同时运行

孤儿进程与僵尸进程:
  • 子进程先于父进程结束,子进程一定会向父进程发送SIGCHLD(17)信号,父进程负责回收子进程的相关资源

  • 如果父进程先于子进程结束,此时子进程称为孤儿进程,同时会被孤儿院进程收养,就成为了孤儿院进程的子进程

    • 早期孤儿院进程init pid是1

    • 现在孤儿院进程不是1了,在图形化界面中是/sbin/upstart --user

  • 子进程先于父进程结束,但是父进程没有去回收子进程相关资源,该子进程就成为僵尸进程

进程标识符:
  • 每个进程都有一个以非负整数表示的唯一标识,称为进程ID,简称PID

  • 进程ID在任意时刻内是唯一的,但是可以重用,当一个进程结束后,它的进程ID就会被分配个后面创建的其他进程使用

  • 延时重用:当进程结束后,它的ID不会立即被系统重新分配,会隔一段时间后再重新分配

#include <sys/types.h>
#include <unistd.h>
​
pid_t getpid(void);
功能:获取当前进程的ID
pid_t getppid(void);
功能:获取当前进程的父进程ID

二、fork创建子进程

#include <unistd.h>
​
pid_t fork(void);
功能:创建一个子进程
返回值:创建失败返回-1
       创建成功会返回两次
       父进程:返回子进程的pid
       子进程:返回0
​
注意:总进程数或者实际拥有pid的进程数量超过了系统的限制,该函数失败

注意:子进程创建出来后,父子进程会同时各自运行代码,因此可以通过分支判断返回值,来让父子进程执行不同的程序代码

#include <stdio.h>
#include <unistd.h>
​
int main(int argc,const char* argv[])
{
    printf("我是进程%u\n",getpid());
​
    pid_t pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        return -1;
    }
​
    if(0 == pid)
    {
        printf("我是子进程%u,我的父进程是%u\n",getpid(),getppid());
        pause();
    }
    else
    {
        printf("我是父进程%u,我的子进程是%u\n",getpid(),pid);
        pause();
    }
}  
父子进程谁先运行:
  • 通过fork系统调用创建出来的子进程与它父进程会各自往下运行,但是其先后顺序不确定,可以通过睡眠等系统调用确定让哪个进程先执行

子进程是父进程的副本:
  • 由fork创建的子进程会获得拷贝出父进程的data段、bss段、heap段、stack段、I/O流缓冲区。

#include <stdlib.h>         
​
int main(int argc,const char* argv[])
{
    int num = 0;
    int* p = malloc(4);
​
    if(fork())
    {
        sleep(1);
        //  父进程
        num = 1000;
        *p = 1000;
    }
    else
    {
        //  子进程
        num = 2000;
        *p = 2000;
    }
//  各自的num都没有被其他进程改变,证明父子进程的num不是同一个,是子进程拷贝了父进程的数据
    //  虽然父子进程中的num 和p的地址是相同的,但是每个进程都拿到4g的虚拟内存,但是映射的物理内存是不一样的,所以虚拟地址相同没有参考价值
    printf("pid=%u : num=%d *p=%d &num=%p p=%p\n",
            getpid(),num,*p,&num,p);
    sleep(2);
    printf("pid=%u : num=%d *p=%d &num=%p p=%p\n",
            getpid(),num,*p,&num,p);
}
​
#include <stdio.h>                                                          
#include <unistd.h>
​
int main(int argc,const char* argv[])
{
    //  *会残留在输出缓冲区,被拷贝给子进程
    //  子进程创建后会继续代码,有可能也会创建子进程
    printf("*");
    
    for(int i=0; i<3; i++)
    {   
        fork();
    }
}
​
子进程会共享父进程的代码段、文件描述符fd:
  • 通过fork创建的子进程会共享父进程的代码段,fork之前的代码只有父进程执行,fork之后的代码父子进程都有机会执行,主要受到逻辑的控制进入不同的分支

  • 不同的程序之间,文件描述符是不能共享的

  • 但是由fork创建的父子进程之间,是把父进程内核中的文件描述符的表格拷贝给了子进程,此时两者共享父进程的已打开的文件描述符

fork子进程会继承父进程的信号处理方式:
  • 通过fork创建子进程会继承父进程的信号处理方式,是因为子进程共享了父进程的代码段

#include <signal.h>
​
void sigint(int num)
{
    printf("我是进程%u,获得了%d信号\n",getpid(),num);
}
​
int main(int argc,const char* argv[])
{
    signal(SIGINT,sigint);
​
    if(fork())
    {
        printf("我是父进程%u\n",getpid());
        for(;;);
    }
    else
    {
        printf("我是子进程%u\n",getpid());
        for(;;);
    }
}

练习1:实现出孤儿进程与僵尸进程,根据ppid和ps命令查看

#include <stdio.h>
#include <unistd.h>
​
int main(int argc,const char* argv[])
{
    pid_t pid = fork();
    if(pid)
    {
        for(;;)
        {
            printf("我是父进程%u,我的子进程是%u\n",getpid(),pid);
            sleep(1);
        }
    }
    else
    {
        printf("我是子进程%u,\n",getpid());
        sleep(3);
        printf("我私了!\n");
    }                                                                                                                                                                                                   
    /*  孤儿进程
    if(fork())
    {
        printf("我是父进程%u\n");
        sleep(3);
        printf("我是父进程我要死了\n");
    }
    else
    {
        for(;;)
        {
            printf("我是子进程%u,我的父进程是%u\n",
                    getpid(),getppid());
            sleep(1);
        }
    }
    */
}

练习2:给主进程创建出4个子进程,再给每个子进程创建2个子进程

#include <stdio.h>
#include <unistd.h>
​
int main(int argc,const char* argv[])
{
    printf("我是主进程%u\n",getpid());
​
    for(int i=0; i<4; i++)
    {
        if(0 == fork())
        {
            printf("我是进程%u,我的父进程%u\n",getpid(),getppid());
            for(int i=0; i<2; i++)
            {
                if(0 == fork())
                {
                    printf("我是孙子进程%u,我的父进程%u\n",
                            getpid(),getppid());
                    pause();
                }
            }
            pause();
        }
    }
    pause();
}  
三、vfork和exec系列函数创建进程
#include <sys/types.h>
#include <unistd.h>
​
pid_t vfork(void);
功能:创建一个子进程,返回值特点与fork没有区别
vfork的特点:
  • 当调用vfork系统调用时,父进程会进入阻塞状态,子进程一定先返回执行

  • 子进程返回时,先临时使用父进程的相关资源,然后等待exec系列函数执行加载一个可执行文件,从而让子进程去启动另一个程序,把那个程序的资源替换自己原来的资源。

  • 当子进程调用完exec系列函数,替换完原来的所有资源后,子进程才算真正创建完毕,此时父进程才会接触阻塞状态,返回子进程的pid

  • 如果子进程不调用exec系列函数后果:

    • 情况1:子进程一直没有创建成功,导致父进程一直处于阻塞状态,无法返回

    • 情况2:子进程直接结束并释放相关资源,此时子进程使用的还是父进程的资源,父进程会返回,但会产生段错误,因为它的相关资源已经被子进程错误释放掉了

  • vfork不能单独创建出子进程,必须与exec系列函数中某个函数配合使用

fork和vfork的区别:
  • vfork调用后,子进程先返回,而fork调用,谁先返回不确定

  • vfork不会复制、共享父进程的相关资源,而是去加载其他程序,替换原来的临时资源

  • 以exec系列函数创建的子进程不会继承父进程的信号处理方式,但是可以继承父进程的信号屏蔽

exec系列函数:
功能:都是为了与vfork配额创建子进程的函数
int execl(const char *path, const char *arg, ...
               /* (char  *) NULL */);
path:要加载的程序的路径
arg:命令行参数,最起码第一个是执行可执行文件的命令,最后一个以NULL结尾
​
int execlp(const char *file, const char *arg, ...
               /* (char  *) NULL */);
file:只需要被加载程序的文件名,系统会根据环境变量PATH中的路径去查找该文件
​
int execle(const char *path, const char *arg, ...
               /*, (char *) NULL, char * const envp[] */);
envp:环境变量表,相当于父进程把自己的环境变量表拷贝给子进程
​
int execv(const char *path, char *const argv[]);
argv:把命令行参数以字符串指针数组方式提供,注意:一定要以NULL结尾
​
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
               char *const envp[]);
#include <stdio.h>
#include <unistd.h>
​
int main(int argc,const char* argv[])
{
    pid_t pid = vfork();
​
    if(0 == pid)
    {
        printf("我是子进程%u\n",getpid());
        //  加载其他程序
        //execl("hello","hello","xixi","10",NULL);
        char* a[] ={"hello","hehe","xx",NULL};
        execv("hello",a);
                                                                                                                           
        printf("-------------\n");
    }
    else
    {
        printf("我是父进程%u\n",getpid());
    }
    printf("************\n");
}
​
四、进程的正常结束
main函数中执行了return 结束进程
int main(...)
{
    ...
    return x;   //  该返回值可以被父进程接收
}
​
//  等价于
int main(...)
{
    ...
    exit(x) //  该返回值可以被父进程接收
}
调用标准C的exit函数 结束进程
#include <stdlib.h>
​
void exit(int status);
status:进程的结束状态码
  • 该函数一旦调用就不会返回,其父进程通过wait\waitpid函数可以获取到status的低8位数据

  • 进程正常退出前会先调用事先通过atexit\on_exit函数注册过的函数,然后冲刷并关闭所有处于打开状态下的标准I/O流

  • 可以用 EXIT_SUCCESS和EXIT_FAILURE常量作为exit或return的结束状态码,表示进程是正常结束,还是出问题结束的

  • 该函数底层调用了 _exit \ _Exit函数

#include <stdlib.h>
int atexit(void (*function)(void));
funciton: 函数指针 进程退出前要执行该函数
​
int on_exit(void (*function)(int , void *), void *arg)
funciton: 函数指针   
    第一个参数:来自return的n或者exit的参数 status
    第二个参数,来自on_exit的arg参数
arg:任意类型指针
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
​
void atexit_fp(void)
{
    printf("我就要死了...\n");
}
​
void on_exit_fp(int s,void* p)
{
    printf("我要结束了...status=%d 遗言%s\n",s,(char*)p);
}
​
int main(int argc,const char* argv[])
{
    //  注册遗言函数
    //  谁后注册,谁先执行
    atexit(atexit_fp);
    on_exit(on_exit_fp,"是它杀了我");
​
    for(int i=0; i<3; i++)
    {
        printf("我是进程%u\n",getpid());
        sleep(1);
    }
​
    exit(10);                                                                                                              
}
​
调用_exit / _Exit 函数结束进程:
#include <unistd.h>
void _exit(int status);
​
#   该函数有一个完全等价的标准C版本
#include <stdlib.h>
void _Exit(int status);
  • 该函数一旦调用就不会返回,其父进程通过wait\waitpid函数可以获取到status的低8位数据

  • 在进程退出前,先关闭所有打开状态下的文件描述符,并把所有的子进程托付给孤儿院进程收养(init\upstart--user),并把当信号SIGCHLD(17)发送给其父进程

其它正常结束进程的方式:
  • 进程的最后一个线程执行完毕,进程也结束

  • 进程的最后一个线程调用pthread_exit函数,进程也结束

五、进程的异常终止
  • 进程收到了某些信号,他杀

  • 进程自己调用abort函数,产生了SIGABRT(6)信号,自杀

  • 进程的最后一个线程收到了"取消"操作,并且做出响应

  • 如果进程是异常结束的,atexit\on_exit它们事先注册的遗言函数不会被调用,也不会冲刷标准IO流

  • 但是依然会给父进程发送信号SIGCHLD(17),关闭所有打开状态下的文件描述符

六、子进程的资源回收
  • 对于子进程任何方式的结束,都希望父进程能够知道,可以通过wait\waitpid函数可以知道子进程是如何结束的以及它结束状态码

pid_t wait(int *status);
功能:以阻塞状态等待任意一个子进程的结束,并回收它的相关资源,获取到结束状态码
status:获取结束的子进程的结束状态码 是输出型参数
返回值:成功返回结束的子进程的pid,失败的返回-1
    1、如果所有子进程都在运行中,则阻塞等待
    2、如果有一个子进程结束,则立即返回该子进程的状态码和pid
    3、如果当前没有子进程运行,返回-1
    
对于子进程的结束状态码status可借助宏函数解析判断:
    WIFEXITED(status)    -  子进程是否正常结束
    WEXITSTATUS(status)  -  当子进程是正常结束时,该宏可以获取到正确的结束状态码的低8位数据
    WTERMSIG(status)    -   如果进程是异常终止的,该宏可以获取到杀死该子进程的信号编号
    
pid_t waitpid(pid_t pid, int *status, int options);

#include <stdio.h>                                                                                                                                                                                      
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
​
int main(int argc,const char* argv[])
{
    for(int i=0; i<10; i++)
    {   
        if(0 == fork())
        {   
            printf("我是子进程%u i=%d\n",getpid(),i);
            sleep(rand()%8+3);
            if(0 == i%2)
            {   
                kill(getpid(),3);
            }
            return 88+i;
        }
    }
    
    for(;;)
    {   
        printf("*\n");
        sleep(1);
        
        int status = 0;
        pid_t pid = wait(&status);
        if(-1 == pid)
        {
            printf("子进程都死了\n");
            break;
        }
        if(WIFEXITED(status))
        {
            printf("是正常结束的 状态码是%d\n",WEXITSTATUS(status));
        }
        else
        {
            printf("是异常终止的,被信号%d杀死\n",WTERMSIG(status));
        }
    }
​
}
  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值