并发编程基础知识(一)[进程篇]

目录

 一.程序的执行方式 

    1.进程是什么东西?   

2.进程与程序区别  

3.进程状态    

4.linux进程地址空间布局   

5.linux下进程相关的API函数  

    (1)创建一个新进程   

    (2)进程退出  

    (3)等待子进程退出 

    (4)让一个进程去执行一个指定的程序文件

    (5)执行指定的程序

                总结:  


 一.程序的执行方式 


    (1)顺序执行  
        一个程序完全执行完毕后,才能执行下一个程序
        假如: 一个程序分为三个步骤:  
                输入数据  --> 计算 ---> 写回文件
            
            缺陷:  
                CPU利用率很低
        
        
    (2)并发执行   
        把一个指令的执行过程,分为几个不同的步骤:   
            取指  --> 执行  -->  写回 
        不同的步骤,由不同的硬件完成。
            
        这样的话,就可以多个程序同时执行。
        
        为了提高cpu的利用率,增加吞吐量。"并发执行"
        
    现代操作系统为了能让程序并发执行,特地引入"进程"的概念    


    
1.进程是什么东西?   


    进程是具有独立功能的程序关于某个数据集合上的一次运行活动。(跑起来的程序)
    test.c   --> 源程序  
        int main()
        {
            int  a, b; 
            int sum;  
            scanf("%d%d",&a,&b);
            
            sum = a + b;
        
            printf("sum = %d\n",sum);
            
            return 0;
        }
        
        gcc test.c -o test =>test 可执行程序  
        
        ./test 

2.进程与程序区别  


    (1)程序是静态概念(是指令的有序集合,"程序文件");
        进程是动态概念(动态产生,动态消亡);
        
    (2)进程是一个程序的一次执行活动,
        一个程序可以对应多个进程
        
    (3)进程是一个独立的活动单位;
        进程是竞争系统资源的基本单位   
        
    例子:    
        进程与程序的区别,就好比"炒菜活动与菜谱"的区别 
    
    OS为什么要引入进程呢?  
        就是为了能让程序并发执行(同一时段有多个程序在运行)
        进程是如何做到让多个程序并发执行的呢?  
        
        程序的并发,实际就是进程的并发。
        进程如何同时运行呢? 进程如何并发的呢? 
        


3.进程状态    

(需要掌握一定的计算机组成原理的知识,PC寄存器存储下一条执行的指令...)

进程在其执行过程中会经历不同的状态。根据经典的进程状态模型,常见的进程状态包括:

1. 创建(Create):当进程被创建但尚未开始执行时,处于创建状态。此时操作系统正在为进程分配必要的资源,并初始化进程的上下文。

2. 就绪(Ready):就绪状态表示进程已准备好运行,但还未分配到 CPU 时间片。进程已经完成了初始化,并等待被调度器挑选为下一个要执行的进程。

3. 运行(Running):运行状态表示进程当前正在执行中。在单处理器系统中,每个时刻只有一个进程能够运行。在多处理器系统中,多个进程可以同时处于运行状态。

4. 阻塞(Blocked):当进程在执行过程中遇到某些阻塞事件(比如等待 I/O 完成、等待锁、等待信号量等)时,进程会进入阻塞状态,暂时停止执行。在这种状态下,进程不会被调度执行,直到等待的事件发生并解除阻塞。

5. 终止(Terminated):当进程的执行结束时,进程进入终止状态。系统会回收该进程所分配的资源,并向其父进程发送退出状态。

需要注意的是,不同的操作系统和调度算法可能会对进程状态模型进行细微的调整或扩展。

此外,还存在一些扩展的进程状态,如挂起(Suspended)、睡眠(Sleeping)等,用于描述一些特定的等待或暂停状态。这些状态在特定的操作系统环境下有特定的含义和用途。

进程状态的转换是由操作系统的调度器来控制的,根据调度算法和各个进程的状态和优先级决定进程的转换和执行顺序。

        
    进程的这些状态之间可以进行切换。

    "就绪队列: Ready Queue"
        所有处于"Ready"状态的进行,都在一个"后备队列"
        
        "调度程序":负责确定下一个进入"Running"状态的进程
        
    "调度策略":调度算法  
        分时系统:调度策略以"时间片轮转"为主要策略的系统  
            “事件片轮转”:分时,每个进程执行一段时间("时间片")
            
            如: 大部分桌面系统,如:linux,andriod,windows,  
                    macos,unix,...
            
        实时系统:调度策略以"实时策略"为主要策略的系统
            “实时策略”:每次调度都去优先级最高的那个进程执行,
                        直到这个进程执行完毕或它主动放弃CPU或其它
                        更高优先级的进程抢占. 
                        
            如:ucos,freeRTOS...
                “抢占”: 插队  
        
    程序的执行过程“进程”  
        程序的执行,分配资源, =》“进程”
        
    程序 = 数据 + 指令   
    
    进程要做的第一件事件,就是申请一块内存区域来存储程序
    的“数据”,不同的"数据"属性是不一样的,"分区域"
    来存储程序的数据 
    


4.linux进程地址空间布局   


    "分段":分不同的逻辑区域
    linux对进程的数据进行分段管理,不同的属性的数据,存储在
    不同的“内存段”中。不同的"内存段(内存区域)"的属性及管理方法
    不一样。
    
    
    .text
        主要存放代码。
        只读并且共享,这段内存在程序运行期间(进程存活期间),不会释放的。
        "代码段" 随进程持续性
    .data
        数据段。  
        主要存放程序的已经初始化的全局变量和已经初始化的static变量。
        可读可写,这段内存在进程运行期间,一直存在。随进程持续性。

    .bss  
        数据段。  
        主要存放程序的没有初始化的全局变量和没有初始化的static变量。
        可读可写,这段内存在进程运行期间,一直存在。随进程持续性。
        .bss段,在进程初始化时,(可能)全部初始化为0.

    .rodata 
        只读数据段。  
        主要存放程序中的只读数据(如:字符串常量)
        只读,这段内存在进程运行期间,一直存在。随进程持续性
        
    
    栈空间(stack)
        主要存放局部变量(非static的局部变量)
        可读可写。这段空间,会自动释放(代码块执行完了,代码块中的局部变量的空间
        就会被自动释放)。随代码块的持续性。
        
        返回一个局部变量的地址,是有问题的,就是这个原因。

    堆(heap)空间:动态内存空间   
        主要malloc/realloc/calloc 动态分配的空间。
        可读可写的,这段内存在进程运行期间,一旦分配,就
        会一直存在,直到你手动free 
        (编程时一般只会接触到堆栈)
        防止“内存泄露”/“垃圾内存”
        
        int main()
        {
            char *s = "12345";  
            //s存放在栈空间  
            //"12345" .rodata 
        }
         
        
        int a;  
        int b = 3;
        int main()
        {
        }
          // a存放在.bss  
          // b存放在.data  
        
        int main()
        {
            char str[] = "12345";
            //str存放在栈空空间
            //"12345" 栈空间 
        }        
        
        int main()
        {
            int *p = malloc(100);
            //p存放在栈空间
            //malloc的空间在堆
        }

5.linux下进程相关的API函数  


    (1)创建一个新进程   


     fork    
        
            fork用来创建一个新进程(child proccess),你要创建一个新进程,首先你得 
            知道一个进程里面包含什么东西?  
                
                系统数据 
                用户数据
                指令 
            fork一个新进程时,这个新进程的 数据 和 指令 来源于哪里呢?
                来源于它爸爸(父进程,调用fork的那个进程)
                
            fork这个函数在创建子进程时:  
                copy了父进程的数据和指令!!!
                父进程的变量,数据对象,
                标准IO缓冲区
                文件描述符
                ...
                copy完了后,父子进程就独立了。
                
            通过fork的不同的返回值,来区分到底是父进程返回,还是子进程返回。
        (注意:fork创建出来的子进程相当于复制了一个父进程,拷贝了父进程里面所有的数据和指令,复制完成之后,父子进程独立)
        
        头文件 
            #include <sys/types.h>
            #include <unistd.h>
        函数功能
            创建一个子进程
        函数原型
             pid_t fork(void);
        函数参数
            无
        函数返回值
            如果失败返回-1,同时errno被设置。
            如果成功:  
                父进程返回 子进程的pid( > 0) 
                子进程返回  0
                
            pid_t fork()
            {
                clone();  //克隆,一旦成功,就会有两个进程往下执行 
                            //父进程  //子进程  
                            
                if(父进程)
                {
                    return 子进程的pid( > 0)
                }                
                else if (子进程)
                {
                    return 0;
                }
            }    
            
        ------
        linux系统会为每一个进程,分配一个唯一的进程id(> 0 的整数),用类型pid_t
        来描述,而且还提供两个函数用于获取当前进程(自己)以及父进程的pid;
        
            头文件 
                #include <sys/types.h>
                #include <unistd.h>
            函数原型 
                pid_t getpid(void);  //用于获取自己的进程pid 
                pid_t getppid(void); //用于获取父进程的pid 


    (2)进程退出  


        进程退出有两种情况:  
        (2.1)自杀 (自己退出)
            a:main函数返回,进程退出
            b:在进程执行的任意,调用进程退出函数
                exit/_exit
                头文件 
                     #include <stdlib.h>
                函数功能 
                    让进程退出,正常退出,做一些清理工作(如:把缓冲区的内容,同步到文件中去)
                    
                函数原型
                    void exit(int status);
                函数参数
                    int status    //表示退出码,表示退出状态 
                                退出码的具体含义,由程序员来解释。    
                函数返回值 
                    无
                
                头文件  
                     #include <unistd.h>
                函数功能
                    _exit 坐火箭走的,让中止进程,来不及做清理工作
                函数原型 
                     void _exit(int status);
                函数参数 
                    int status    //表示退出码,表示退出状态 
                                退出码的具体含义,由程序员来解释。
                函数返回值 
                    无
        
            例子:  
                1.分析如下程序的输出结果 
                    int main()
                    {
                        printf("hello world");
                        _exit(-3);
                    }
                    终端没有任何输出
                    int main()
                    {
                        printf("hello world ");
                        exit(0);
                    }
                    终端输出hello world 
        
        
        
        (2.2)被操作系统干掉了
            比如我们的段错误之类的。

    (3)等待子进程退出 


        wait   
        waitpid 
        头文件  
            #include <sys/types.h>
            #include <sys/wait.h>
        函数功能
            等待子进程退出 
        函数原型 
            pid_t wait(int *wstatus);

            pid_t waitpid(pid_t pid, int *wstatus, int options);
            
        这两个函数用来等待某个(些)子进程的状态发生改变的,等待的状态
        发生改变有三种情况:   
            a.子进程退出(正常退出):main函数返回值/exit/_exit 
            b.子进程被信号中止 
            //c.子进程被信号唤醒(blocking -> ready)
            
        在子进程正常退出(a)情况,调用wait/waitpid可以释放子进程的资源
        假如没有调用wait/waitpid,那么子进程退出后,就会变成僵尸进程(zomble)
            一个进程退出,操作系统会释放他大部分的资源,但是有一部分
            必须留给他的父进程去释放。如果一个进程退出了,但是它父进程没有
            wait/waitpid,这个进程就会变成僵尸进程:已经死掉了 但是资源没有
            被完全释放掉。
            
        假如一个子进程的状态已经发生改变,那么调用wait/waitpid就会立即返回,    
        否则会阻塞调用进程直到某个子进程的状态发生改变或被信号中断。

        pid_t wait(int *wstatus); 用来等待任意一个子进程退出的状态    
        函数参数 
            int *wstatus  //指针。指向的空间,用来保存子进程的退出信息的
            (怎么死的,退出码等等)
            
            wstatus 用来保存退出的子进程的退出信息的,退出信息保存在一个整数。
            我们可以用如下宏来解析子进程的退出信息:  
                WIFEXITED(wstatus)
                    return  true 
                    假如该子进程是正常退出的(main返回/exit/_exit)
                    只有子进程正常退出,它才会有退出码!!!
                    
                    WEXITSTATUS(wstatus)
                        返回子进程的退出码,只有子进程正常退出 
                        这个宏才有意义,
                        把进程的退出码, unsigend char 来看待 
                    
                WIFSIGNALED(wstatus)
                    return true  
                    假如子进程是被信号干掉的
                    ...
        
        
    
    
            pid_t waitpid(pid_t pid, int *wstatus, int options);  
                pid_t pid   //指定要等待的进程或进程组
                            pid == -1,表示等待任意的子进程退出
                            pid == 0,表示等待与调用进程同组的任意子进程
                            "进程组" 
                                就是一组进程。每个进程必须会属于某一个进程组。
                                并且每个进程组,都会有一个组长进程,一般来说, 
                                创建这个进程组的进程为组长,进程组有一个组id,
                                这个组id,就是组长进程的pid,
                            pid < -1 表示等待组id等于 pid绝对值的那个组的任意子进程
                            
                            如:  
                                pid == -128  
                                等待进程组 128那个组内的任意的子进程 
                                
                            pid > 0,表示等待指定的子进程(其进程id为pid的那个进程)

                int *wstatus  //同上。  

                
                int options     //等待选项 
                             0:表示阻塞等待 
                             WNOHANG:非阻塞,假如没有子进程退出,则立即返回。
                
                wait(&wstatus)
                    <=> waitpid(-1,&wstatus,0);
                
            函数返回值 
            成功:返回退出的那个子进程的进程id   
            失败:返回-1,同时errno被设置。
            (父进程创建了子进程之后,子进程默认会继承父进程的进程组。所以他的孙子,孙孙子,都会在最开始创建它爸爸的组里。main函数的进程
            组由操作系统分配)
            
    fork一个子进程,一般来说,是让子进程去执行其它的任务      
        (不是说创建的子进程就必须要在父进程里面用wait等待,有时候父进程也要做自己的事情,你在子进程调用Exit退出时,只需要手动清理
        文件描述符,套接字,堆内存就极少情况下会出现僵尸进程。不用刻意在主进程里调用wait等待子进程的退出)


    (4)让一个进程去执行一个指定的程序文件


        exec函数族 主要作用是 让一个进程去执行一个指定的程序文件。
        或者这么理解:exec函数族的作用就是让一个指定的程序文件中的 
        数据和指令替换到 调用进程 的数据和指令
        
        exec函数族是让一个进程去指定另外一个程序文件。
        就是让另外一个程序文件的数据和指令,覆盖当前进程的数据和指令。  
        
        
        exec让一个进程去执行另外一个程序,那么:  
            你是不是要指定这个程序文件的名字? 
                一个子文件系统中的程序文件的名字(带路径)
            你可能还得指定程序运行的参数!!!
                在linux下程序的参数,都是字符串
                
                指定程序的参数有两种方式:  
                    l:list 
                        把程序运行的参数,一个一个地列举出来。
                        程序运行的第一个参数,是程序的名字(不带路径)
                        gcc sum.c -o sum 
                        
                        "sum","3","4",NULL

                    v:vector 向量,数组
                        把程序运行的参数,弄成一个char *数组  
                        char *arg[] = {"sum","3","4",NULL};
        头文件
            #include <unistd.h>
        函数功能
            让进程去执行参数指定的程序文件。
        函数原型
            int execl(const char *path, const char *arg, ...);
        函数参数 
            const char *path        //程序文件的文件名(带路径的)
            const char *arg, ...    //程序运行的参数,以列表的形式给 
                    如:
                        "sum","3","4",NULL

                        
        函数返回值:  
            失败: 返回-1,同时errno被设置。
            成功:就永远不会返回了!!!因为你的整个指令和数据段,
                    都被人家替换掉了,还怎么返回呢?
        

            execv与execl作用、功能、返回值都是一样的。 
            唯一不同的是,指定的程序文件的参数方式不一样。
            
        函数原型 
            int execv(const char *path, char *const argv[]); 
        函数参数  
            const char *path        //要执行的程序文件名(带路径)
            char *const argv[]        //指定程序运行的参数。程序运行的第一个参数
                                    是程序名,最后一个为NULL,表示参数结束了。
            
            系统中有一个环境变量 PATH
                环境变量:是整个系统环境内所有进程共享的变量。
                有很多的环境变量,其中一个环境变量叫  PATH 
                    PATH:=dir1:dir2:dir3 ...
                PATH的作用是,指定命令或程序的搜索路径,什么意思呢? 
                当你只指定了一个命令或程序的文件名,而没有指定路径时,
                那么系统首先会在dir1这个目录下去查找,如果找到,则执行,
                没有找到,则在dir2这个目录下查找,...全部找完没找到就会
                报错。
                
                意思是说,如果你的程序文件或命令已经在PATH指定的搜索目录下
                的时候,你指定文件时就没有必要指定路径了。
                
                
                p:path    
                    指定的程序文件在标准的搜索路径下面  
                    
                    execlp 
                    execvp 
                    
                int execlp(const char *file, const char *arg, ...);    
                    file:要执行的程序文件名(可以不带路径)
                    arg:...    
                int execvp(const char *file, char *const argv[]);                    
                    ...
        ----
            fork() 
                -->exec 
                    
            有一个一步到位的函数 

    (5)执行指定的程序


        头文件 
            #include <stdlib.h>
        函数功能
            system用来执行command指定的命令或程序
            system会等待命令或程序执行完成才返回
        函数原型
            int system(const char *command);    
        函数参数
            const char *command    //要指定的命令字符串的地址
        函数返回值 
            system函数对返回值的处理,设计三个阶段: 
                1.创建子进程等准备工作,如果失败,返回值-1;
                2.调用某个可执行程序,如果没有找到可执行程序或者可执行程序
                    未正常运行。原因值被写入到返回值的低8-15比特中。系统提供了宏;
                    WEXITSTATUS(返回值)。如果WEXITSTATUS(返回值)为真,则说明正常结束
                3.如果可执行程序正常执行结束,将可执行程序返回填到返回值的8-15比特位中。
                    系统提供了宏;WEXITSTATUS(返回值)
        综上所述  
            判断一个sytem函数调用可执行程序是否正常结束的方法应该是 
            如下3个条件同时成立: 
                (1) -1 != status
                (2)WEXITSTATUS(status)为真 
                (3)调用的可执行程序在正常结束后返回的那个返回值或者说是
                    退出码 == WEXITSTATUS(status)
                            
                            
            madplay xxx.mp3
            
            例子: 
                system("madplay xxx.mp3");
                or  
                char cmd[256] = {0}; 
                sprintf(cmd,"madplay %s",p->mp3name);
                system(cmd);           

            
    总结:  


        (1)进程的概念 
           进程与程序的区别 
           
        (2)进程的状态切换 
        
        (3)调度策略 
            分时系统 
            
            实时系统 
        
            抢占  
                "就绪队列"
                
        (4)fork实现原理  
            a.clone  
            b.独立了
            
        (5)exec* 函数族    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值