Linux进程

进程相关概念

程序和进程

程序:程序是静态的概念,gcc xxx.c -o pro,磁盘中生成pro文件叫做程序

进程:进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程

如何查看系统中有哪些进程

ps -aux | grep init		查看init相关进程
top

进程标识符:每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证
pid=0 交换进程(swapper) 为进程调度
pid=1 init进程 为系统初始化

并发

并发是多个任务交替执行的,多个任务之间可能还是串行的。所有的并发处理都有排队等候,唤醒和执行这三个步骤。所以并发是宏观的观念,在微观上他们都是序列被处理的,只不过资源不会在某一个上被阻塞(一般是通过时间片轮转),所以在宏观上多个几乎同时到达的请求同时在被处理。如果是同一时刻到达的请求也会根据优先级的不同,先后进入队列排队等候执行。并发针对的是多个请求,比如:一个CPU,一个web服务,同时涌入多个请求,CPU需要交替切换的执行多个请求,而不是一个请求执行完成之后再执行下一个请求。并发的实质是一个物理CPU(也可以是多个物理CPU)在若干个程序之间多路复用,并发性是对有限物理资源强制行使 多用户共享以提高效率。

虚拟内存和虚拟地址空间

虚拟内存: 虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。

虚拟地址空间: 在32位的i386 CPU的地址总线的是32位的,也就是说可以寻找到4G的地址空间。我们的程序被CPU执行,就是0x000000000xFFFFFFFF这一段地址中。高1G的空间为内核空间,由操作系统调用,低3G的空间为用户空间,由用户使用。
CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。因为只有程序的一部分加入到内存中,所以会出现所寻找的地址不在内存中的情况(CPU产生缺页异常),如果在内存不足的情况下,就会通过页面调度算法来将内存中的页面置换出来,然后将在外存中的页面加入到内存中,使程序继续正常运行。
在这里插入图片描述

为了在多进程环境下,使得进程之间的内存地址不受影响,相互隔离,于是操作系统就为每个进程独立分配一套虚拟地址空间,每个程序只关心自己的虚拟地址就可以,实际上大家的虚拟地址都是一样的,但分布到物理地址内存是不一样的。作为程序,也不用关心物理地址的事情。


每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出)(这个就是虚拟内存),在需要的时候再装载回物理内存(换入)。


每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出)(这个就是虚拟内存),在需要的时候再装载回物理内存(换入)。


那既然有了虚拟地址空间,那必然要把虚拟地址「映射」到物理地址,这个事情通常由操作系统来维护。


那么对于虚拟地址与物理地址的映射关系,可以有分段和分页的方式,同时两者结合都是可以的。


内存分段是根据程序的逻辑角度,分成了栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。但是每个段的大小都不是统一的,这就会导致内存碎片和内存交换效率低的问题。


于是,就出现了内存分页,把虚拟空间和物理空间分成大小固定的页,如在 Linux 系统中,每一页的大小为 4KB。由于分了页后,就不会产生细小的内存碎片。同时在内存交换的时候,写入硬盘也就一个页或几个页,这就大大提高了内存交换的效率。


再来,为了解决简单分页产生的页表过大的问题,就有了多级页表,它解决了空间上的问题,但这就会导致 CPU 在寻址的过程中,需要有很多层表参与,加大了时间上的开销。于是根据程序的局部性原理,在 CPU 芯片中加入了 TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度。


Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理。于是 Linux 就把所有段的基地址设为 0,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。另外,Linxu 系统中虚拟空间分布可分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。

进程控制块PCB

我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是 task_struct 结构体。
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h 文件中可以查看 struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:
在这里插入图片描述

进程状态

进程基本的状态有 5 种。分别为新建态,就绪态,运行态,阻塞态与终止态。其中新建态为进程准备阶段,常与就绪态结合来看
在这里插入图片描述

  • NULL→新建态:执行一个程序,创建一个子进程。
  • 新建态→就绪态:当操作系统完成了进程创建的必要操作,并且当前系统的性能和虚拟内存的容量均允许。
  • 运行态→终止态:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结。
  • 运行态→就绪态:运行时间片到;出现有更高优先权进程。
  • 运行态→等待态:等待使用资源;如等待外设传输;等待人工干预。
  • 就绪态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
  • 等待态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
  • 终止态→NULL:完成善后操作。

创建进程

基本API

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);失败返回-1;成功返回:父进程返回子进程的 ID(非负)如果是子进程返回 0
pid_t getpid(void);获取当前进程 ID
pid_t getppid(void);获取当前进程的父进程 ID
pid_t vfork(void);失败返回-1;成功返回:父进程返回子进程的 ID(非负)如果是子进程返回 0

补充: 初学者常常有个问题,就是子进程的执行范围:其实子进程只会继续执行fork函数之后的部分…

vfork和forl的区别
1)fork(): 父子进程的执行次序不确定。
vfork():保证子进程先运行,在它调用 exec(进程替换) 或 exit(退出进程)之后父进程才可能被调度运行。

2)fork(): 子进程拷贝父进程的地址空间,子进程是父进程的一个复制品。
vfork():子进程共享父进程的地址空间(准确来说,在调用 exec(进程替换) 或 exit(退出进程) 之前与父进程数据是共享的)

案例程序

打印各自的pid号

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

int main()
{
    pid_t pid;
     
    pid = fork();
    if(pid>0)  //如果大于0,大于0的数表示为创建子进程的pid号  这是父进程
    {
        printf("father:My father pid = %d\n",getppid());   //打印父进程pid
        printf("father:My child pid = %d\n",pid);   //打印子进程pid
        printf("father:My pid = %d\n",getpid());    //打印自己的pid
    }
    else if(pid==0){    //这是子进程
        printf("child:My father pid = %d\n",getppid()); //打印父进程pid
        printf("child:My pid = %d\n",getpid()); //打印自己的pid
    }
    return 0;
}

模拟网络请求

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    pid_t pid;
    char input[100];
    int data;

    while (1)
    {
        printf("请输入一个整数: ");   //打印提示信息
        if (fgets(input, sizeof(input), stdin) == NULL) {    //获取输入数据
            printf("读取输入错误.\n");
            exit(EXIT_FAILURE);   //这里表示程序执行失败,一般为非0整数
        }

        data = atoi(input);  //将输入数据装换成整数

        if (data == 0 && strcmp(input, "0\n") != 0) {    //如果data为0并且input里面的数据大于"0\n"则判断转换失败
            printf("输入非法: %s\n", input);
            continue;
        }

        if(data==1){         //判断数字是否为1
            pid = fork();   //创建进程
            if(pid>0)  //如果大于0,大于0的数表示为创建子进程的pid号  这是父进程
            {

            }
            else if(pid==0){    //这是子进程
                while(1){
                    printf("进行网络请求,进程号:%d\n", getpid());   //打印进程号
                    sleep(3);
                }
            }
        }
        else{
                printf("wait,do nothing\n");
        }
    }

    return 0;
}

vfork案例

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    int cnt = 0;    
    pid = vfork();  //创建进程
    if(pid>0){  //如果大于0,大于0的数表示为创建子进程的pid号  这是父进程
        while(1){
            printf("father:cnt : %d pid : %d\n",cnt,getpid());
            sleep(3);
        }
    }
    else if(pid==0){        //这是子进程
        while(1){
                cnt++;
                printf("child:cnt : %d pid : %d\n",cnt,getpid());
                if(cnt>=3){         //运行三次结束自己的进程
                   exit(0);
                }
                sleep(3);
        }
    }
    return 0;
}


应用场景

  • 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的-----父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
  • 一个进程要执行不同的程序。这对shell使常见的情况。在这种情况下,子进程从fork返回后立即调用exec

进程退出

相关概念

正常退出:

  1. Main函数调用return
  2. 进程调用exit(),标准C库
  3. 进程调用_exit()或者_Exit(),属于系统调用

异常退出:

  1. 调用abort
  2. 最后一个线程调用pthread_exit
  3. 最后一个线程从启动历程返回
  4. 接到一个信号并终止
  5. 最后一个线程对取消请求做出响应

exit()与_exit()的区别
在这里插入图片描述

return与exit的区别
主要来说exit是系统调用级别的,代表着一个进程的结束。它会删除进程的内存空间,同时把错误信息返回给父进程,通常情况,exit(0)表示正常退出,exit(1)和exit(-1)表示程序异常退出,exit(2)表示程序找不到指定的文件。在整个程序中,只要调用exit就结束。
return是语言级别的,代表着调用堆栈的返回。如果在main函数中代表着结束本进程,如果不是,则返回上一层调用。

通常情况,exit(0)表示正常退出,exit(1)和exit(-1)表示程序异常退出,exit(2)表示程序找不到指定的文件。在整个程序中,只要调用exit就结束。

程序API

#include <stdlib.h>
#include <unistd.h>

void exit(int status);
void _exit(int status);
void _Exit(int status);

案例程序

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    int cnt = 0;    
    pid = vfork();  //创建进程
    if(pid>0){  //如果大于0,大于0的数表示为创建子进程的pid号  这是父进程
        while(1){
            printf("father:cnt : %d pid : %d\n",cnt,getpid());
            sleep(3);
        }
    }
    else if(pid==0){        //这是子进程
        while(1){
                cnt++;
                printf("child:cnt : %d pid : %d\n",cnt,getpid());
                if(cnt>=3){         //运行三次结束自己的进程
                   exit(0);
                }
                sleep(3);
        }
    }
    return 0;
}

父进程等待子进程退出

相关概念

明确创建子进程的目的:
在这里插入图片描述
子进程的退出状态:
(1)不被收集

僵尸进程:一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并由 init 进程对它们完成状态收集工作。

(2)被收集
wait,waitpid,waitid 收集子进程退出状态的API

进程等待函数waipid

相关API

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);//*status 地址
pid_t waitpid(pid_t   pid,   int   *status,   int options);
int  waitid(idtype_t  idtype,  id_t  id, siginfo_t *infop, int options);

1.非空:子进程退出的状态放在它所指向的地址中。
2.空:不关心退出状态。
3.返回值:如果成功,返回终止子进程的ID;错误,-1

关于linux的进程状态参考
Linux进程状态解析 之 R、S、D、T、Z、X
参数:

  • status:输出型参数,获取子进程的退出状态码
  • pid:pid = -1时,表示等待任意一个子进程;pid > 0,等待指定子进程
  • options:

0:阻塞等待,默认行为(也就是干等着子进程运行完,什么都不干)
在这里插入图片描述
**WNOHANG:**非阻塞等待,相当于隔一小段时间去检测子进程是否结束,如果结束了,
则回收子进程;如果没有结束,则继续忙自己的事
在这里插入图片描述

返回值:
回收子进程以后,返回收集到的子进程的pid
如果设置了WNOHANG,若检测到子进程还在运行,则返回0
若检测到子进程运行结束,返回子进程的pid

关于参数status

status不能简单的当作整型来看,要从二进制的角度来看,32位下,整型转化为二进制有32个bit位
但是我们仅关注低16位

(1) 正常退出时
进程正常退出时,子进程会返回退出码,即退出状态,8-15位记录着正常退出时的退出码,既然是正常退出就不会收到中止信号,所以0~7位都是 0
在这里插入图片描述
(2) 异常退出时
进程异常退出时,一般会收到一个中止进程的信号,而且不会执行到return 这句,所以自然就没有退出码,为了知道发生了何种异常,我们使用低 7 位,也就是 0~6 来记录 “中止信号”

如何取出退出码和中止信号

方式一:

中止信号是低7位,也就是0~6 的位置,我们可以让status按位与0x7F,那么0111 1111 中1对应的位置会被保留 —>status & 0x7F
退出码是高8位,我们先将这8位二进制退出码右移8位到 较低的8位,然后像上面那样按位与0xFF
那么1111 1111 中1对应的位置会被保留 ——> (status >> 8) & 0xFF

方式二:

使用系统提供的宏函数WIFEXITED(status)、 WEXITSTATUS(status)

WIFEXITED(status) :判断进程是否正常退出,如果正常退出,返回true
WEXITSTATUS(status): 返回子进程的退出码
WIFSIGNALED(status):判断子进程是否被信号结束
WTERMSIG(status):获取结束子进程的信号类型

案例程序

wait阻塞等待

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

int main()
{
    pid_t pid;
    int cnt = 0;
    int status = 10;
    pid = fork();
    if(pid>0){      //如果大于0,大于0的数表示为创建子进程的pid号  这是父进程
        wait(&status);  //死等子进程退出  这里如果为空不关心退出状态
        printf("child quit,status = %d\n",WEXITSTATUS(status));   //取出状态码
        while(1)
        {
            printf("cnt=%d\n",cnt);
            printf("this is father print pid = %d\n",getpid());
            sleep(1);
        }
    }
    else if(pid==0){        //这是子进程
        while(1) 
        {
            printf("this is child print pid = %d\n",getpid());
            sleep(1);
            cnt++;
            if(cnt>=3)      //运行三次结束自己的进程
            {
              exit(0);
            }
        }
    }

    return 0;
}

非阻塞的方式查询

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    int cnt = 0;
    int status = 10;
    pid = fork();        //创建进程
    if(pid>0){
        //wait(&status);
        int ret = waitpid(pid,&status,WNOHANG);  //非阻塞的方式程序进程是否结束
        while(ret==0){    //不断轮询
            printf("father: polling\n");
            ret = waitpid(pid,&status,WNOHANG);
            sleep(1);
        }
        //子进程退出
        printf("child quit,status = %d\n",WEXITSTATUS(status));
        while(1)
        {
            printf("cnt=%d\n",cnt);
            printf("this is father print pid = %d\n",getpid());//获取进程pid号
            sleep(1);
        }
    }
    else if(pid==0){
        while(1) 
        {
            printf("this is child print pid = %d\n",getpid());      //获取进程pid号
            sleep(1);
            cnt++;
            if(cnt==3)       //运行三次结束自己的进程
            {
              exit(0);
            }
        }
    }
    return 0;
}

exec族函数

可参考以下文章
linux进程—exec族函数(execl, execlp, execle, execv, execvp, execvpe)

示例程序
execl

//文件execl.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path, const char *arg, ...);

int main(void)
{
    printf("before execl\n");
    if(execl("./echoarg","echoarg","abc",NULL) == -1)
    {
        printf("execl failed!\n");      
    }
    printf("after execl\n");
    return 0;
}
//文件echoarg.c
#include <stdio.h>

int main(int argc,char *argv[])
{
    int i = 0;
    for(i = 0; i < argc; i++)
    {
        printf("argv[%d]: %s\n",i,argv[i]); 
    }
    return 0;
}

实验说明:
我们先用gcc编译echoarg.c,生成可执行文件echoarg并放在当前路径bin目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。

execlp可运行环境变量,当然也兼容上一个demo功能

//文件execlp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execlp(const char *file, const char *arg, ...);
int main(void)
{
    printf("before execlp****\n");
    if(execlp("ps","ps","-l",NULL) == -1)
    {
        printf("execlp failed!\n");
    }
    printf("after execlp*****\n");
    return 0;
}

带v不带l的一类exac函数,包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。

//文件execvp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execvp(const char *file, char *const argv[]);

int main(void)
{
    printf("before execlp****\n");
    char *argv[] = {"ps","-l",NULL};
    if(execvp("ps",argv) == -1) 
    {
        printf("execvp failed!\n");     
    }
    printf("after execlp*****\n");
    return 0;
}

带e的一类exac函数,包括execle、execvpe,可以传递一个指向环境字符串指针数组的指针。 参数例如char *env_init[] = {“AA=aa”,”BB=bb”,NULL}; 带e表示该函数取envp[]数组,而不使用当前环境。
下面以execle函数为例:

//文件execle.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execle(const char *path, const char *arg,..., char * const envp[]);

char *env_init[] = {"AA=aa","BB=bb",NULL};
int main(void)
{
    printf("before execle****\n");
        if(execle("./echoenv","echoenv",NULL,env_init) == -1)
        {
                printf("execle failed!\n");
        }       
    printf("after execle*****\n");
    return 0;
}
//文件echoenv.c
#include <stdio.h>
#include <unistd.h>
extern char** environ;
int main(int argc , char *argv[])
{
    int i;
    char **ptr;
    for(ptr = environ;*ptr != 0; ptr++)
        printf("%s\n",*ptr);
    return 0;
}

system函数

system(执行shell 命令)
相关函数
fork,execve,waitpid,popen
表头文件
#include<stdlib.h>
定义函数
int system(const char * string);
函数说明
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。
返回值
如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。若参数string为空指针(NULL),则返回非零值。如果 system()调用成功则最后会返回执行shell命令后的返回值,但是此返回值也有可能为system()调用/bin/sh失败所返回的127,因此最好能再检查errno 来确认执行成功。
附加说明
在编写具有SUID/SGID权限的程序时请勿使用system(),system()会继承环境变量,通过环境变量可能会造成系统安全的问题。
注意,system() 函数的返回值是命令的退出状态码。如果命令执行成功,返回值为0,否则为非0值。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    pid_t pid;
    char input[100];
    int data;

    while (1)
    {
        printf("请输入一个整数: ");   //打印提示信息
        if (fgets(input, sizeof(input), stdin) == NULL) {    //获取输入数据
            printf("读取输入错误.\n");
            exit(EXIT_FAILURE);   //这里表示程序执行失败,一般为非0整数
        }

        data = atoi(input);  //将输入数据装换成整数

        if (data == 0 && strcmp(input, "0\n") != 0) {    //如果data为0并且input里面的数据大于"0\n"则判断转换失败
            printf("输入非法: %s\n", input);
            continue;
        }

        if(data==1){         //判断数字是否为1
            pid = fork();   //创建进程
            if(pid>0)  //如果大于0,大于0的数表示为创建子进程的pid号  这是父进程
            {

            }
            else if(pid==0){    //这是子进程
                   int ret = system("ls");
                   if(ret!=0){
                    printf("命令执行失败\n");
                   }
                    exit(1);
            }
        }
        else{
                printf("wait,do nothing\n");
        }
    }

    return 0;
}

popen函数

提到system函数,就不得不提到popen函数,根据system函数的源代码:system函数的执行需要通过调用fork()函数创建一个子进程,子进程通过execl函数调用shell对传参的可执行文件进行实现。这也意味着system函数实现需要依赖execl函数实现自身功能。因此system函数的结果将直接显示在终端上,这样原本运行的结果就无法保存在文件中用于实现信息交互等功能。

NAME
       popen, pclose - pipe stream to or from a process
 
SYNOPSIS
       #include <stdio.h>
 
       FILE *popen(const char *command, const char *type);
 
       int pclose(FILE *stream);

参数说明:
commmand:是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。

type:只能是读和写的一种,如果是 “r” 则文件指针连接到command的标准输出,则返回的文件指针是可读的;如果是 “w” 则文件指针连接到command的标准输入,则返回的文件指针是可写的。

stream:popen返回的文件指针。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    char ret[1024]={0};
    FILE *fp;
     
    fp = popen("ps","r");
    int nread = fread(ret,1,1024,fp);

    printf("read ret %d byte, ret = %s\n",nread,ret);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值