简明Linux教程-Linux下的进程

8 进程

8.1 程序/进程/CPU相关

程序:死的,只占用磁盘空间; ----剧本

进程:活的,运行起来的程序,占用内存/CPU等资源 ----演出

一个进程可以同时在多处进行运行;

并发:在操作系统中,一个时间段中由多个进程都处于已启动运行到运行完毕之后的状态;但同一个时刻点只有一个进行在运行;

单道程序设计(串行)/多道程序设计(并行)

多道程序设计必须由硬件设计基础作为保证

CPU的量级是ns级的,

时钟中断即为多道程序设计模型的基础;

存储介质,越往下,速度越慢,容量越大。

一个寄存器,4个字节,32位机器的情况下;

MMU在CPU内部,虚拟内存映射单元;用于将指针存储的虚拟地址转换成实际的地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l3qlcvRQ-1642599317427)(/home/daniel/.config/Typora/typora-user-images/image-20210514141826927.png)]

8.2 虚拟内存和真实内存之间的映射关系

0~4G的地址是虚拟地址,4G范围是可用范围;真实内存可以是小于等于 4G

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wcICWPbx-1642599317429)(/home/daniel/.config/Typora/typora-user-images/image-20210514142504513.png)]

虚拟地址>>>>>>>>>>>MMU>>>>>>>>>>内存(真实物理地址)

内存大小以4K为单位;映射是分块映射,单位是4K

一个进程除内核空间之外的内容会映射到内存中与其它进程不同的区域,而所有成的PCB控制块,都属于操作系统,会映射到同一片区域;

MMU还可以用来设置内存访问区权限(将空间分为内存空间和内核空间)

Linux系统映射分为两个级别,0级和3级,0级代表内核空间,3级代表用户空间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrY8grkI-1642599317429)(/home/daniel/.config/Typora/typora-user-images/image-20210514222150738.png)]

8.3 PCB进程控制块

linux中进程控制块的位置:/usr/src/linux-headers-5.4.0-72/include/sched.h

进程控制块的结构体名称:struct task_struct{…}

进程控制块需要重点掌握内容:

1)进程id。系统每个进程的都有独立的运行id,在C中用pid_t表示,其实是一个非负整数;

ps aux ps aux|grep content

2)进程状态,由就绪(初始化和就绪),运行,挂起,停止等状态;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzd5lvFK-1642599317430)(/home/daniel/.config/Typora/typora-user-images/image-20210514151550770.png)]ec

3)进程切换时需要保存一些CPU寄存器;

4)描述虚拟地址空间的信息;

5)描述控制终端的信息;

6)当前目录(current working directory);(例如使用 ls .或者ls …)

7)umask 掩码 是进程的概念

比如:使用两个shell,一个shell的umask设置为003,另外一个shell的默认umask并不会改变,说明umask是shell的PCB中存储的属性;

8)文件描述符表,包含很多指向file结构体的指针;

文件描述符表>>>>>>文件描述符>>>>>>FILE

9) 和信号相关的信息;

10)用户id和组id;

11)会话(session)和进程组;

  1. 进程可以使用的资源上限(Resource Limit)

8.4 环境变量

特点:环境变量的实质时字符串,其字母一般是大写的

格式:name=value

作用:值用来描述环境信息;

LD_LIBRARY_PATH 指明动态连接库的位置;

PATH 记录可执行文件目录位置;

例如:使用shell中的各种命令,就是PATH规定了寻找位置

echo $PATH

SHELL 记录当前使用shell是哪个 echo $SHELL

TERM 当前终端类型 echo $TERM

LANG 语言和locale echo $LANG

HOME 家目录 echo $HOME

env 命令,查看所有环境变量

环境变量和main函数的命令行参数均存放在3~4G之前的那块位置,就是栈的顶端

8.5 进程控制

8.5.1 fork()函数

函数名:pid_t fork(void)

功能:创建子进程

头文件:#include <sys/type.h> #include <unistd.h>

参数:void

返回值:成功创建进程,父进程返回子进程的pid,子进程返回0;创建失败,父进程返回-1,子进程未被创建,没有返回值,同时设置erron;

#include <unistd.h>
#include <stdio.h>

int main(int argc, char** argv)
{
    printf("-----Befor fork!-----\n");
    int pid;
    pid=fork();
    if(pid==-1){
        perror("fork failed!\n");
    }
    else if(pid==0){
        printf("----this is child process!-----\n");
        printf("my pid is %d\n",getpid());
        printf("my parent's pid is %d\n",getppid());

    }
    //主进程的父进程是//bin/bash
    else if(pid>0){
        printf("-----this is parent process!-----\n");
        printf("my pid is %d\n",getpid());
        printf("my parent's pid is %d\n",getppid());
    }
    printf("-----this is the end of file-----\n");
    return 0;
}

父进程在子进程之前,先执行。

daniel@daniel-Vostro-5471:~/文档/OS/test/fork$ ./fork
-----Befor fork!-----
-----this is parent process!-----
my pid is 25840
my parent's pid is 23741
-----this is the end of file-----
----this is child process!-----
my pid is 25841
my parent's pid is 25840
-----this is the end of file-----
daniel@daniel-Vostro-5471:~/文档/OS/test/fork$ ps aux | grep 23741
daniel     23741  0.0  0.0  13932  4616 pts/2    Ss   09:02   0:00 /bin/bash
daniel     25860  0.0  0.0  11992   664 pts/2    S+   09:38   0:00 grep 23741

函数: pid_t getpid(void) 获取本进程的id pid_t getppid(void) 获取本进程父进程的id

头文件: #include<sys/types.h> #include <unistd.h>

参数:void

返回值:总是成功,返回进程id

实际运行中可以把bash看成在bash运行程序的父进程;

8.5.2 进程并发

代码:创建5个子进程

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char **argv)
{
    int i;
    __pid_t pid;
    for (i = 0; i < 5; i++)
    {
        if (fork() == 0)
        {
            break;
        }
    }
    if (i == 5)
    {
        sleep(1);
        printf("我是父进程\n");
    }
    else
    {
        printf("我是进程%d\n", i);
    }
    return 0;
}

getuid() 获取用户id getgid() 获取组id

8.5.3 父子进程共享

父子进程在fork之后,有那些相同?那些不同?

fork之后,

父子相同之处:全局变量/.data/.text/栈/堆/环境变量/用户ID/宿主目录/进程工作目录/信号处理方式

不同之处:进程id/父进程ID/fork返回值/进程运行时间/闹钟(定时器)/未决信号集

父子进程共享:文件描述符,mmap建立的映射区

父子进程遵循 读时共享,写时复制 的原则 应用在全局变量时

fork之后,父子进程哪个先执行,或者多个子进程哪个先执行,不确定,取决于内部算法;

父子进程不共享全局变量

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int var=0;
int main(int argc, char** argv)
{
    __pid_t pid;
    pid=fork();
    if(pid<0){
        perror("create fork failed!\n");
    }
    if(pid==0){
        var=200;
        printf("child, var=%d\n",var);
        printf("my pid %d, my parent %d\n",getpid(),getppid());
    }
    if(pid>0){
        var=288;
        printf("parent,var=%d\n",var);
        printf("my pid %d, my parent %d\n",getpid(),getppid());
    }
    return 0;
}

8.5.4 gdb调试多进程程序

使用gdb对多进程程序进行调整的时候,gdb默认只能跟踪1个进程,可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者跟踪子进程。默认跟踪父进程

set follow-fork-mode child 命令设置gdb在fork之后,跟踪子进程

set follow-fork-mode parent 设置之后跟踪父进程

注意:一定要在fork函数调用之前,调用才有效;

8.5.5 exec函数族

有六种以exec开头的函数,统称为exec函数族;

fork创建子进程后,执行的是和父进程相同的程序(但有可能是不同的代码分支),子进程往往调用exec函数执行另外一个程序,当进程调用exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动历程开始执行。调用exec并不创建新的进程,所有调用exec之后,进程的id不会改变。

函数能够将当前进程的.text段和.data段,切换成所要加载程序的只读数据区和代码区,然后让进程从新的.text 第一条指令开始执行,但进程ID不便,换核不换壳。

其中有六种以exec开头的函数,统称为exec函数族;

函数:

int execl(const char *pathname, const char arg, … / (char *) NULL */);
int execlp(const char *file, const char arg, …/ (char *) NULL */);
int execle(const char *pathname, const char arg, …/, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

头文件:#include <unistd.h>

功能:在一个进程中执行另外一个程序

The  exec() family of functions replaces the current process image with a new process image.  The functions described in this manual  page  are front-ends  for execve(2).  (See the manual page for execve(2) for further details about the replacement of the current process image.)
The initial argument for these functions is the name of a file that  is to be executed.
The  functions can be grouped based on the letters following the "exec" prefix.

参数:char* pathname 代表程序路径 char* file 程序名字

​ arg 是程序执行的选项,可填写多个,是传递给第一个参数用的,注意从arg[0]开始填写;

​ NULL 必须要填写,是哨兵,帮助程序判断参数传递是否结束;

返回值:正常执行时,没有返回值;执行错误,返回-1,设置erron;

//execl函数
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv)
{
    int pid;
    pid=fork();
    if(pid==-1){
        perror("fork failed!\n");
    }
    else if(pid==0){
        //execl("./hello","hello",NULL);
        execl("/bin/ls","ls","-l",NULL);
        perror("exec error!\n");
        exit(1);
    }
    else if(pid>0){
        //使用sleep堵塞主进程,让子进程先执行
        sleep(1);
        printf("the pid of mine %d\n",getpid());
    }
    printf("-----this is the end of file-----\n");
    return 0;
}
//execlp函数
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv)
{
    int pid;
    pid=fork();
    if(pid==-1){
        perror("fork failed!\n");
    }
    else if(pid==0){
        //NULL  哨兵
        //execlp("ls","-l","-d","-h",NULL); 错误的写法,缺少argv[0]参数
        //execlp("ls","ls","-l","-h",NULL);  正确写法
        execlp("date","date",NULL);
        //如果上面执行成功,不会执行下面的程序
        perror("exec error\n");
        exit(1);
    }
    else if(pid>0){
        sleep(1);
        printf("the pid of mine %d\n",getpid());
    }
    printf("-----this is the end of file-----\n");
    return 0;
}

exec函数族一般规律:

1.一旦成功,执行新程序,不返回。只有失败,返回-1,所以通常值加添加perror或者exit,而不进行判断;

2.L(list) 命令行参数

​ P(path) 搜索file时,使用path变量

​ V(vector) 使用命令行参数数组

​ e(enviroment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量

​ 实际上,只有execve是真正的系统调用,其它均依赖于execve,

​ man 2 execve man 3 其它exec函数

8.5.6 孤儿进程&僵尸进程

孤儿进程:父进程先于子进程结束,则子程序成为孤儿进程。子进程的父进程成为init进程,称为init进程领养孤儿进程。

僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB)存放在内核中,变成僵尸进程; kill命令对子进程无效

ps ajx 查看进程,包括进程的父进程;

孤儿进程demo:

#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    printf("-----Befor fork!-----\n");
    int pid;
    pid = fork();
    if (pid == -1)
    {
        perror("fork failed!\n");
    }
    else if (pid == 0)
    {
        while (1)
        {
            //父进程运行完毕,但是子进程一直在运行
            //子进程成为了孤儿进程
            sleep(1);
            printf("my pid is %d\n", getpid());
            printf("my parent's pid is %d\n", getppid());
        }
    }
    else if (pid > 0)
    {
        printf("-----this is parent process!-----\n");
        sleep(9);
        printf("my pid is %d\n", getpid());
        printf("my parent's pid is %d\n", getppid());
    }
    printf("-----this is the end of file-----\n");
    return 0;
}
//使用kill -9 子进程pid	可以把子进程kill掉

僵尸进程:

#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    printf("-----Befor fork!-----\n");
    int pid;
    pid = fork();
    if (pid == -1)
    {
        perror("fork failed!\n");
    }
    else if (pid == 0)
    {
        printf("----this is child process!-----\n");
        sleep(10);
        printf("my pid is %d\n", getpid());
        printf("my parent's pid is %d\n", getppid());
    }
    else if (pid > 0)
    {
        //子进程运行完毕之后,父进程一直在运行,没有时间回收子进程PCB
        //子进程变为僵尸进程
        while (1)
        {
            printf("-----this is parent process!-----\n");
            printf("my pid is %d\n", getpid());
            printf("my parent's pid is %d\n", getppid());
            sleep(1);
        }
    }
    printf("-----this is the end of file-----\n");
    return 0;
}
//使用 kill -9 父进程id	将父进程杀死,让init将僵尸进程回收

8.5.7 wait函数回收子进程

函数:pid_t wait(int *wstatus)

头文件:#include <sys/types.h> #include <sys/wait.h>

作用:1)堵塞父进程,等待子进程执行完毕;

​ 2)将wstatus传出,并且返回子进程id;

​ 3)让父进程回收子进程的pid;

参数: int *wstatus 传出参数 整型变量的指针

返回值:pid_t 子进程的pid号

demo:

使用处理wstatus的宏,

W_IFEXITED() W_EXITSTATUS() 判断是否正常退出,及子进程的返回值;

W_IFSIGNALED() W_TERMSIG() 判断是否信号终止,及终止信号的类型

W_IFSTOPPED() WSTOPSIG() WIFCONTINUED() 判断是否挂起,以及挂起信号,和挂起之后是否正常运行

#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    printf("-----Befor fork!-----\n");
    pid_t pid, w_pid;
    int state;
    pid = fork();
    if (pid == -1)
    {
        perror("fork failed!\n");
    }
    else if (pid == 0)
    {
        printf("----this is child process!-----\n");
        printf("my pid is %d\n", getpid());
        printf("my parent's pid is %d\n", getppid());
        sleep(20);
        printf("I am going to die\n");
        //子进程的返回的状态
        return 68;
    }
    else if (pid > 0)
    {
        //wait参数可以为NULL,说明父进程不关心子子进程终止原因
        w_pid = wait(&state);
        printf("child is finished,the id of child %d\n", w_pid);
        if (WIFEXITED(state))
        {//如果正常退出,返回子进程的退出状态,也就是子进程返回的value
            printf("the child exit status %d\n", WEXITSTATUS(state));
        }
        if (WIFSIGNALED(state))
        {//如果被信号终止,打印终止程序运行的信号值
            printf("the terminate signal is %d\n", WTERMSIG(state));
        }
    }
    printf("-----this is the end of file-----\n");
    return 0;
}

kill -l命令可以查看所有的中断信号类型;一共由64种;

daniel@daniel-Vostro-5471:~/文档/OS/test/fork$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

函数: pid_t waitpid(pid_t pid,int *wstatus, int options)

头文件:#include <sys/type.h> #include <sys/wait.h>

参数:

​ 一次wait/waitpid函数调用,只能回收一个字进程;

​ pid_t pid 指定回收的进程pid,有几个不同的值

	0:meaning  wait  for  any child process whose process group ID is equal to that of the calling process at the time of the call to waitpid().
	>0:meaning wait for the child whose process ID is equal  to  the  value  of pid.

​ -1:meaning wait for any child process.

	<0:	meaning wait for the child whose process ID is equal  to  the  value  of pid.		

​ 默认父进程分裂出的子进程属于一个组,但是可以通过系统待调用进行更改;

​ status:传出回收进程的状态

​ options:WNOHANG 指定回收方式:非阻塞

返回值:

​ >0:表示成功回收子进程pid;

​ 0:函数调用时,参3指定了WNOHANG,并且没有子进程结束;

​ -1:失败,errno[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GKb3e5M6-1642599317431)(/home/daniel/.config/Typora/typora-user-images/image-20210516172726360.png)]

DEMO:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
    int i;
    __pid_t pid, pid_w,pid_temp;
    for (i = 0; i < 5; i++)
    {
        if ((pid_w=fork()) == 0)
        {
            break;
        }
        if(i==2){
            pid_temp=pid_w;
        }
    }
    if (i == 5)
    {
        //读时共享,写时复制,所以父进程读取不到pid_w的值
        printf("我是父进程,我在等待回收子进程%d\n",pid_temp);
        //wpid=waitpid(-1,NULL,WNOHUNG);
        waitpid(pid_temp,NULL,0);//阻塞回收
        //waitpid(pid_temp,NULL,WNOHANG);//不阻塞回收
        printf("我回收子进程成功\n");
    }
    else
    {
        printf("我是进程%d,我的pid是%d\n", i,getpid());
    }
    return 0;
}

总结:不论wait和还是waitpid,一次调用只能回收一个子进程

想要回收多个子进程,可以通过循环调用waitpid()函数;循环回收子进程DEMO:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
    int i;
    __pid_t pid, pid_w, pid_temp;
    for (i = 0; i < 5; i++)
    {
        if ((pid_w = fork()) == 0)
        {
            break;
        }
        if (i == 2)
        {
            pid_temp = pid_w;
        }
    }
    if (i == 5)
    {
        /*while((pid_w=waitpid(-1,NULL,0))!=-1){
            printf("回收进程%d成功\n",pid_w);
        }*/
        while ((pid_w = waitpid(-1, NULL, WNOHANG)) != -1)
        {
            sleep(1);
            printf("回收进程%d成功\n", pid_w);
        }
    }
    else
    {
        printf("我是进程%d,我的pid是%d\n", i, getpid());
    }
    return 0;
}

8.5.8 练习题代码

//一个父进程fork三个子进程
//一个调用ps命令
//一个调用自定义程序1
//一个调用自定义程序2(出现段错误)
//使用waitpid进行回收
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>

int main(int argc, char **argv)
{
    __pid_t pid, pid_w;
    int i = 0, state,ret;
    for (; i < 3; i++)
    {
        //创建三个进程,当fork
        if ((pid = fork()) == 0)
        {
            break;
        }
    }//进程1执行ps命令  正常
    if ((i == 0) && pid == 0)
    {
        printf("I'm child %d,my pid is %d\n",i+1,getpid());
        execlp("ps", "ps", NULL);
    }//进程2执行hello   正常
    else if ((i == 1) && (pid == 0))
    {
        printf("I'm child %d,my pid is %d\n",i+1,getpid());
        execl("./hello", "hello", NULL);
    }//进程3执行error   出现段错误
    //error是一个试图修改只读字符串的小程序
    else if ((i == 2) && pid == 0)
    {
        printf("I'm child %d,my pid is %d\n",i+1,getpid());
        ret=execl("./error", "error", NULL);
        perror("execl error\n");
    }//父进程
    if (pid>0)
    {//堵塞回收三个进程
        while ((pid_w = waitpid(-1, &state, 0))!=-1)
        {
            printf("I'm recycle %d\n", pid_w);
            if (WIFEXITED(state))
            {
                printf("exit state:%d\n", WEXITSTATUS(state));
            }
            else if (WIFSIGNALED(state))
            {
                printf("signal:%d\n", WTERMSIG(state));
            }
        }
    }
    return 0;
}

程序执行结果:

daniel@daniel-Vostro-5471:~/文档/OS/test/fork$ ./wait_prac
I'm child 1,my pid is 29028
I'm child 2,my pid is 29029
I'm child 3,my pid is 29030
hello_world!
hello_exec!
hello, I'm error!
I'm recycle 29029
exit state:0
    PID TTY          TIME CMD
   4481 pts/2    00:00:00 bash
  29027 pts/2    00:00:00 wait_prac
  29028 pts/2    00:00:00 ps
  29030 pts/2    00:00:00 error
I'm recycle 29028
exit state:0
I'm recycle 29030
signal:11

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值