当前位置:首页 » 《随便一记》 » 正文

Linux系统编程 | 【03】进程、环境变量、IPC_Jxiepc的博客

27 人参与  2021年11月16日 11:03  分类 : 《随便一记》  评论

点击全文阅读


文章目录

      • 一、进程相关概念
        • 1、程序和进程
        • 2、并发
        • 3、单道程序设计
        • 4、多道程序设计
        • 5、CPU和MMU
        • 6、进程控制块PCB
        • 7、进程状态
      • 二、环境变量
        • 1、常见环境变量
          • 1.1 PATH
          • 1.2 SHELL
          • 1.3 TERM
          • 1.4 LANG
          • 1.5 HOME
        • 2、相关函数
          • 2.1 getenv
          • 2.2 setenv
          • 2.3 unsetenv
      • 三、进程控制
        • 1、fork
        • 2、getpid
        • 3、getppid
        • 4、getuid
        • 5、getgid
        • 6、进程共享
        • 7、gdb调试
      • 三、exec函数
        • 1、execlp
        • 2、execl
      • 四、回收子进程
        • 1、孤儿进程
        • 2、僵尸进程
        • 3、wait函数
        • 4、waitpid函数
        • 5、案例
      • 五、IPC方法
        • 1、管道
          • 1.1 pipe
          • 1.2 案例
        • 2、共享存储映射
          • 2.1存储映射I/O
          • 2.2 mmap
          • 2.3 munmap
          • 2.4 常见问题
          • 2.5 案例
          • 2.6 mmap父子进程通信
          • 2.7 匿名映射
          • 2.8 mmap无血缘关系进程间通信

一、进程相关概念

1、程序和进程

  • 程序:编译好的二进制文件;
  • 进程:抽象概念,活跃的程序,占用资源,在内存中执行;
  • 同一程序可加载为不同的进程;

2、并发

一个时间段中有多个进程都处于启动到结束之间的状态,在任一时刻都只有一个进程在运行。

3、单道程序设计

一次只能运行一个程序;

4、多道程序设计

  • 同时存放几道相互独立的程序;
  • 首先必须要有支持该硬件的基础;
  • 时钟中断是其基础:强制让进程让出cpu,1秒可执行10亿条指令;

5、CPU和MMU

cpu工作原理

将命令传入到预取器中,后转到译码器进行译码,在传入算数逻辑单元执行,再将处理好的数据传到寄存器堆,最后再传回寄存器;

在这里插入图片描述

存储介质

在这里插入图片描述

MMU工作原理

  • 位于cpu内部;
  • 虚拟内存与物理内存的映射;
  • 设置修改内存访问级别;

6、进程控制块PCB

每个进程在内核中都有一个PCB来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体;

struct task_struct

  • 进程id;
  • 状态;
  • 进程切换时需要保存和恢复的一些cpu寄存器;
  • 描述虚拟地址空间的信息;
  • 描述控制终端的信息;
  • 当前工作目录;
  • umask掩码;
  • 文件描述符表,包含很多指向file结构体的指针;
  • 和信号相关的信息;
  • 用户id和组id;
  • 会话和进程组;
  • 进程可以使用的资源上限;

在这里插入图片描述

7、进程状态

初始态、就绪态、运行态、挂起态、终止态;

二、环境变量

  • 在操作系统中用来指定操作系统运行环境的一些参数;
  • 位置:位于用户区,高于stack的起始位置;
  • 引入环境变量表:需要声明环境变量,extern char ** environ
  • 存储形式:char *[] environ,NULL为结尾;

特征

  • 字符串;
  • 格式:名=值[:值](多个值,则用冒号隔开);
  • 值用来描述进程环境信息;

1、常见环境变量

1.1 PATH
  • 可执行文件的搜索路径。
  • 例:ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名/a.out,由于PATH环境变量的值里面包含了Is命令所在的目录/bin,却不包含a.out所在的目录。PATH 环境变量的值可以包含多个目录,用:号隔开。
  • 在Shell中用echo命令可以查看这个环境变量的值:$ echo $PATH
1.2 SHELL

当前shell:/bin/bash

1.3 TERM
1.4 LANG
1.5 HOME

2、相关函数

2.1 getenv

获取环境变量值

char *getenv(const char *name);
char *secure_getenv(const char *name);

/*
* return: 返回一个指向环境中该值的指针,如果不匹配则返回NULL;
*/
2.2 setenv

设置/添加环境变量的值;

int setenv(char *name, const char *value, int overwrite)

/*
* @param overwrite: 1时,覆盖原环境变量,0时,不覆盖;
* return:成功返回0,失败返回-1;
*/
2.3 unsetenv

删除环境变量name的定义

int unsetenv(const char *name)

/*
* return: 成功返回0,失败返回-1,name不存在仍返回0
*/

三、进程控制

1、fork

通过复制创建一个子进程

pid_t fork(void);

/*
* return: 失败返回-1,成功返回:父进程返回子进程的ID以及子进程返回0
*/

2、getpid

返回调用进程的进程ID。 (生成唯一临时文件名的例程经常使用此方法)

pid_t getpid(void);

3、getppid

返回调用进程的父进程的进程ID。

pid_t getppid(void);

4、getuid

返回调用进程的真实用户ID。

uid_t getuid(void);

uid_t geteuid(void);
// 获取当前进程有效id

5、getgid

返回调用进程的真实ID。

gid_t getgid(void);

6、进程共享

父子进程间,遵循读时共享写时复制原则;

fork后的相同之处

  • .data
  • .text
  • 栈;
  • 堆;
  • 环境变量;
  • 用户ID;
  • 宿主目录;
  • 进程工作目录;
    信号处理方式;

相同之处

  • 进程ID;
  • fork返回值;
  • 父进程ID;
  • 进程运行时间;
  • 定时器;
  • 未决信号集;

共享

  • 文件描述符(打开文件的结构体);
  • mmap建立的映射区。

7、gdb调试

gdb调试只能跟踪一个进程。可在fork函数调用前,通过gdb调试跟踪进程或其子进程,默认跟踪父进程;

  • set follow-fork-mode child 命令设置gdb在fork之后跟踪父进程;
  • set follow-fork-mode parent 设置跟踪父进程;

三、exec函数

子进程要调用一种exec函数执行另一个程序,并退出该程序;

  • l:命令行参数列表;
  • p:搜索file时使用path变量;
  • v:使用命令参数数组;
  • e:使用环境变量数组;

1、execlp

加载一个进程,借助PATH环境变量;

int execlp(const char *file, const char*arg...);

/*
eg: execl('ls', 'ls', '-l', '-a', NULL);
* @param file: 要加载的程序名。配合PATH来使用,当在PATH中所有目录搜索后无该参数则返回-1;
* return: 只在错误的时候返回-1
*/

2、execl

加载一个进程;与execpl的差别就在于第一个参数;

int execl(const char *path, const char *arg, ...);
/*
* @param path: 路径+程序名;
* return: 成功无返回,失败返回-1;
*/

四、回收子进程

1、孤儿进程

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

2、僵尸进程

  • 进程终止,父进程尚未收回,子进程残留PCB在内核中;
  • kill不能终止僵尸进程,因为僵尸进程已终止。

3、wait函数

父进程可调用wait获取,在清除该进程。

  • 阻塞等待子进程退出;
  • 回收子进程残留资源;
  • 获取子进程结束状态(退出原因)。
pid_t wait(int *status);


/*
* @param status: 为传出参数,子进程状态;
* 	- WIFEXITED为(status)非0 - 进程正常结束;
* 	- WEXITSTATUS(status) - 上一个宏为真,则获取进程退出状态,exit的参数;
* 	- WIFSIGNALED(status) - 进程异常终止;
* 	- WTERMSIG(status) - 上一个宏为真,则取得使进程终止的那个信号的编号;
* return: 返回终止子进程的进程ID; 出错时,返回-1;
*/

4、waitpid函数

作用与wait相同,指定pid进程清理,可不阻塞;

pid_t waitpid(pid_t pid, int *status, int options);

/*
* @param pid: 
* 	>0 - 回收指定ID的子进程;
* 	-1 - 回收任意子进程(=wait);
* 	 0 - 回收和当前调用waitpid一个组的所有子进程;
*  <-1 -回收指定进程组内的任意子进程;
* return: 成功返回清理的子进程,失败返回-1(无子进程),返回0参数3为WONHANG,且子进程正在运行;
*/

5、案例

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

int main(){
    pid_t pid, wpid;
    int status;
    //for(int i=0; i<5; i++)
    pid = fork();

    if(pid == 0){
        // 子进程, getppid获取父进程ID
        std::cout << "我是子进程,父进程为:" << getppid() << std::endl; 
        //sleep(20);
        exit(1);
    }else if(pid == -1){
        // 失败
        std::cout << "error" << std::endl;
    }else{
        wpid = wait(&status);
        // 异常退出
        if(WIFSIGNALED(status) != 0){
            std::cout << "子进程的异常退出状态:" << WTERMSIG(status) << std::endl;
        } 
        // 正常退出
        if(WIFEXITED(status)){
            std::cout << "子进程的正常退出状态:" << WEXITSTATUS(status) << std::endl;
        }
        while(1){
            std::cout << "父进程ID:" << getpid() << "子进程为L: "<< pid << std::endl;
            sleep(1);
        }
    }
    return 0;
}

五、IPC方法

Linux下进程是相互独立的,不能相互访问。若要交换数据则需要通过内核,在内核开辟一块缓冲区,将进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,该机制为IPC进程间通信

常用的通信方式

  • 管道 - 最简单;
  • 信号 - 开销最小;
  • 共享映射区 - 无血缘关系;
  • 本地套接字 - 最稳定。

1、管道

  • 其本质是一个伪文件(实为内核缓冲区4k);
  • 由俩个文件描述符引用,一个读端,一个写端
  • 规定数据从管道的写端流入管道,从读端流出

局限性

  • 数据自己读不能自己写
  • 数据被读走,便不在管道中存在,不可反复读取
  • 由于管道采用半双工通信方式。因此,数据只能再一个方向上流动;
  • 只能再有公共祖先的进程间使用管道;

常用通信方式

  • 单工通信:只能发生信号 - 遥控器;
  • 半双工通信:单向发生或接收 - 微信;
  • 全双工通信:双向发生接收 - 通话;
1.1 pipe

创建一个管道;

int pipe(int pipefd[2]);

/*
* @param pipefd:
* 	pipefd[0]指的是管道的读端
* 	pipefd[1]指到管道的写入端
* return: 成功返回0,失败返回-1
*/
1.2 案例
#include<stdlib.h>
#include<unistd.h>
#include<iostream>
#include<cstring>
/*
 *  1、创建子进程,管道
 *  2、将子进程设为读、父进程设为写
 *  3、通过管道实现进程间通信
 * */
int main(int argc, char *argv[]){
    pid_t pid;
    int fd[2];

    int ret = pipe(fd);
    if(ret == -1){
        std::cout << "pipe create error..." << std::endl;
        exit(1);
    }

    pid = fork();
    if(pid == -1){
        std::cout << "child process error..." << std::endl;
        exit(1);
    }else if(pid == 0){ // 子进程
        std::cout << "child process id: " << getpid() << std::endl;
        close(fd[1]); // 关闭写操作
        char buf[1024];
        ret = read(fd[0], buf, 1024);
        if(ret == -1){
            std::cout << "file read error..." << std::endl; 
            exit(1);
        }
        // 将读到的数据输出
        write(STDOUT_FILENO, buf, ret);
    }else{ // 父进程
        close(fd[0]); // 关闭读操作
        std::string myStr = "test pipe\n";
        write(fd[1], myStr.c_str(), myStr.size());
    }

    return 0;
}

2、共享存储映射

2.1存储映射I/O
  • 使一个磁盘文件与存储空间中的一个缓冲区相映射(相当于从缓冲区取数据)。类似于,将数据存入缓冲区,则相应字节就自动写入文件,即使用I/O地址操作;
  • 首先应通知内核,将一个指定文件映射到存储区域,可通过mmap函数实现;
2.2 mmap
void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

/*
* @param addr: 建立映射区的首地址,有Linux内核指定,使用时,传入NULL即可;
* @param length: 指定映射的长度;
* @param prot:映射区权限;
* 	- PROT_NONE: 
* 	- PROT_EXEC: 可能被执行;
* 	- PROT_READ: 读取;
* 	- PROT_WRITE: 写;
* 	- PROT_NONE: 无法访问;
* @param flags: 标志位参数(用于设定更新物理区域、设置共享、创建匿名映射区)
* 	- MAP_SHARED:会将映射区所做的操作反映到物理设备上;
* 	- MAP_PRIVATE:映射区所做的修改不会反映到物理设备;
* @param fd:用来建设映射区的文件描述符;
* @param offset:映射文件的偏移(4k的整数倍);
* return: 返回一个指向映射区域的指针,失败返回MAP_FAILED;
*/
2.3 munmap

系统调用删除指定地址范围的映射,并导致对在产生无效内存引用的范围内的地址。 该区域也会自动取消映射进程终止。

int munmap(void *addr, size_t length);

/*
* @param addr: map的返回值;
* return: 失败返回0;
*/
2.4 常见问题
  • 映射区大小不能为0;
  • 创建映射区的权限要 <= 文件打开的权限,映射区的创建隐含着对文件的读操作;
  • offset必须是4k的整数倍;
  • 映射区的释放与文件关闭无关,只要映射建立成功,文件即可关闭
  • munmap传入的第一个参数一定是mmap返回的地址;
2.5 案例
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <cstring>

/*
 * 1、打开文件,并获取文件大小
 * 2、创建映射区
 * 3、
 * */
int main(int argc, char *argv[]){

    char *p = NULL;
    int fd = open("test.txt", O_CREAT|O_RDWR, 0644); // 打开/创建文件
    if(fd < 0){
        std::cout << "open error..." << std::endl;
        exit(1);
    }

    // 拓展文件大小
    int ret = ftruncate(fd, 4);
    if(ret == -1){
        std::cout << "ftruncate error..." << std::endl;
        exit(1);
    }
    // 创建映射区,注意c++中需要强转
    p = (char *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED){
        std::cout << "mmap error..." << std::endl;
        exit(1);
    }

    std::cout << "-----------------------" << std::endl;
    strcpy(p, "test\n");
    ret = munmap(p, 4);
    if(ret == -1){
        std::cout << "munmap error..." << std::endl;
        exit(1);
    }
    std::cout << "over............." << std::endl;
    close(fd);

    return 0;
}

2.6 mmap父子进程通信

血缘关系的进程可通过mmap建立的映射区来完成数据通信,需要设置对应的flag:

  • MAP_PRIVATE:(私有映射)父子进程各自独占映射区;
  • MAP_SHARED:(共享映射)父子进程共享映射区;
  • 结论
    • 打开的文件;
    • mmap建立的映射区,需要用MAP_SHAPED

案例

#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<sys/mman.h>

int t_val = 1; 

/*
*结论:
*    打开的文件;
*    mmap建立的映射区,需要用`MAP_SHAPED`
*/
int main(int argc, char *argv[]){

    pid_t pid; // 存储fork返回的子进程ID
    
    int fd = open("temp.txt", O_RDWR|O_CREAT|O_TRUNC, 0644);
    if(fd < 0){
        perror("open error");
        exit(1);
    }
    /*
     *  删除临时文件目录项,让所有占用该文件的进程都结束后被删除
     * */
    unlink("temp.txt"); 
    ftruncate(fd, 4);

    int *p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

    if(p == MAP_FAILED){
        perror("mmap error");
        exit(1);
    }

    close(fd); // 映射区建立好即可关闭

    pid = fork();
    if(pid == 0){
        // 子进程返回
        *p = 1000;
        t_val = 200;
        std::cout << "child: " << getpid() << "*p: " << *p << "t_val: " << t_val << std::endl;
    }else if(pid < 0){
        perror("fork error");
        exit(1);
    }else{
        sleep(1);
        std::cout << "parent: " << getppid() << "  child: " << pid <<  "  *p: " << *p << "  t_val: " << t_val << std::endl;
        wait(NULL);
        
        int ret = munmap(p, 4);
        if(ret == -1){
            perror("munmap error");
            exit(1);
        }
    } 
    std::cout << "---------------" << std::endl;
    return 0;
}

2.7 匿名映射

无需依赖一个文件即可创建映射区,且需要标志位来flag来指定;

  • flag只需或上:MAP_ANONYMOUS/MAP_ANON
  • 文件描述符使用-1代替;
  • 是linux系统特有的;

类UNIX系统中

  • 需要借助 fd = open("/dev/zero", O_RDWR)
  • 该文件无大小;
2.8 mmap无血缘关系进程间通信

写文件

#include<iostream>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/mman.h>
#include<cstring>
#include<stdio.h>


/*@ 学生结构体 */
typedef struct STU{
    int id;
    char name[10];
    char sex;
}stu;

/* 写文件
 * 
 * */
int main(int argc, char *argv[]){
    
    STU student = {10, "xiaoming", 'm'};
    char *mm;
    
    if(argc < 2){
        std::cout << "./a.out file_shared" << std::endl;
        exit(-1);
    }
    // 打开文件
    int fd = open(argv[1], O_RDWR|O_CREAT, 0644);
    ftruncate(fd, sizeof(student));

    // 建立映射区
    mm = (char *)mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(mm == MAP_FAILED){
        perror("mmap error");
        exit(-1);
    }
    
    close(fd);

    while(1){
        memcpy(mm, &student, sizeof(student));
        student.id++;
        std::cout << "write..." << std::endl;
        sleep(1);
    }

    munmap(mm, sizeof(student));
    return 0;
}

读文件

#include<iostream>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<cstring>
#include<sys/mman.h>
#include<stdio.h>
/*@ 学生结构体  */
typedef struct stu{
    int id;
    char name[10];
    char sex;
}STU;


/* 读文件
 * 
 * */
int main(int argc, char *argv[]){
    
    STU student;
    STU *st;

    if(argc < 2){
        std::cout << "a.out file_sharead\n" << std::endl;
        exit(-1);
    }

    // 只读模式,打开文件
    int fd = open(argv[1], O_RDONLY);
    if(fd == -1){
        perror("open error");
        exit(-1);
    }

    // 建立映射,建立通信
    st = (STU *)mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
    if(st == MAP_FAILED){
        perror("mmap error");
        exit(-1);
    }
    close(fd);
    
    // 打印数据
    while(1){
        std::cout << "id: " << st->id << "name: " << st->name << "性别: " << st->sex << std::endl;
        sleep(2);
    }
    // 终止映射
    munmap(st, sizeof(student));

    return 0;
}


点击全文阅读


本文链接:http://zhangshiyu.com/post/30931.html

进程  映射  返回  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1