系统编程 学习笔记 01

本文围绕进程展开,介绍了进程相关概念,如程序与进程区别、并发及多道程序设计等。阐述了环境变量的特征、常见类型及相关操作函数。还讲解了进程控制原语,包括fork、getpid等函数,以及父子进程共享情况和exec函数族的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程相关的概念

程序

是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁…)

进程

是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行。同一个程序也可以加载为不同的进程(彼此之间互不影响)

并发:(时钟中断 硬件手段)

单道程序设计

多道程序设计

  • 时钟终端即为多道程序设计模型的理论基础。并发时,任意进程在执行期间都不希望放弃 cpu。因此系统需要一种强制让进程让出 cpu 资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。操作系统中的中断处理函数,来负责调度程序执行。

  • 在多道程序设计模型中,进程分时复用 cpu 资源。实质上,并发是宏观并行,围观串行!

cpu/mmu:(中央处理器/内存管理单元)

  • mmu:(按照page单位分配内存)
    • 虚拟内存与物理内存的映射(pcb位于同一块物理内存里)
    • 设置修改内存访问级别

进程控制块PCB:(task_struct结构体)

  • 重点掌握如下:
    • 进程id,系统中的每个进程有唯一的id,在 C 语言中用 pid_t 类型表示,其实就是一个非负整数。
    • 进程的状态,有初始,就绪,运行,挂起,终止等状态
    • 进程切换时需要保存和恢复一些 CPU 寄存器的值
    • 描述虚拟地址空间的信息。
    • 描述控制终端的信息
    • 当前工作目录(current working directory)
    • umask掩码
    • 文件描述表,包含很多指向 file 结构体的指针
    • 和信号相关的信息
    • 用户 id 和组 id
    • 会话(Session)和进程组
    • 进程可以使用的资源上线(Resource )

环境变量

环境变量,是指在操作系统中用来指定操作系统运行环境的一些参数。通常具备一下特征。

  • 字符串(本质)
  • 有统一的格式: 名=值[:值]
  • 值用来描述进程环境信息

存储形式:与命令行参数类似。char *[]数组,数组名 environ, 内部存储字符串,NULL作为哨兵结尾。

使用形式:与命令行参数类似。

加载位置:与命令行参数类似。位于用户区,高于 stack 的起始位置。

引入环境变量表:须声明环境变量。extern char ** environ;

练习:打印当前进程的所有环境变量。

#include <stdio.h>

extern char **environ;

int main()
{
    int i;
    for(i = 0; environ[i]; i++) {
        printf("%s\n", environ[i]);
    }
    return 0;
}

常见的环境变量

  • PATH

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

    • 当前 Shell,它的值通常是 /bin/bash。
  • TERM

    • 当前终端类型,在图形界面终端下它的值通常是 xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
  • LANG

    • 语言和 locale,决定了字符编码以及时间、货币等信息的显示格式
  • HOME

    • 当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。

getenv 函数

获取环境变量的值

  • char *getenv(const char *name); 成功: 返回环境变量的值; 失败: NULL(name 不存在)

练习:编程实现 getenv 函数

#include <stdio.h>

extern char **environ;

char *getenv(const char *name) {
    printf("%s\n", name);
    int i, j;
    for(i = 0; environ[i]; i++) {
        //printf("%s\n\n", environ[i]);
        for(j = 0; environ[i][j]; j++) {
            if(environ[i][j] != name[j]) {
                break;
            }
        }
        if(environ[i][j] == '=') {
            return environ[i];
        }
    }
    return NULL; 
}
int main()
{
    char *s = getenv("PATH");
    if(s != NULL) {
        printf("%s\n", s);
    }
    else printf("-1\n");
    return 0;
}

setenv 函数

设置环境变量的值

  • int setenv(const char *name, const char *value, int overwrite); 成功 0; 失败 -1;
  • 参数 overwrite 取值:
    • 非 0: 覆盖原环境变量
    • 0: 不覆盖。(该参数常用于设置新环境变量,如: ABC = haha-day-night)

unsetenv 函数

删除环境变量 name 的定义

  • int unsetenv(const char *name); 成功: 0; 失败: -1
  • 注意事项: name 不存在仍返回 0(成功),当 name 命名为 “ABC=” 时则会出错。

练习:熟悉上述三个函数的使用

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char *val;
    val = getenv("ABC");
    printf("env:%s,val:%s\n", "ABC", val);
    if(setenv("ABC", "lala", 1) == -1) {
        perror("set error");
        exit(1);
    }
    val = getenv("ABC");
    printf("env:%s,val:%s\n", "ABC", val);
    if(unsetenv("ABC") == -1)
    {
        perror("unset error");
        exit(1);
    }
    val = getenv("ABC");
    printf("env:%s,val:%s\n", "ABC", val);
    
    return 0;
}

进程控制原语

fork 函数

创建一个子进程

  • pid_t fork(void);
    • 失败返回 -1;
    • 成功返回:
      • 父进程返回子进程的 ID(非负)
      • 子进程返回 0

getpid 函数

获取当前进程的 pid

  • pid_t getpid(void);

getppid 函数

获取当前进程的父进程 pid

  • pid_t getppid(void);

练习:创建 5 代进程

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int now;
void dfs()
{
    if(now == 5) return ;
    printf("pid==%d, ppid==%d\n", getpid(), getppid());
    pid_t pid = fork();
    if(pid == -1) {
        perror("fork error");
        exit(1);
    }
    else if(pid == 0) {
        now++;
        dfs();
    }
}
int main(void)
{
    printf("start\n");
    dfs();
    sleep(1);
    return 0;
}

getuid 函数

获取当前进程实际用户 ID

  • uid_t getuid(void);

获取当前进程有效用户 ID

  • uid_t geteuid(void);

getgid 函数

获取当前进程实际用户组 ID

  • gid_t getgid(void);

获取当前进程有效用户组 ID

  • gid_t getegid(void);

进程共享

父子进程之间在 fork 后。有哪些相同,哪些异同之处呢?

刚 fork 之后:

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

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

父子进程间遵循读时共享写时复制的原则,目的节省内存开销

【重点】父子进程共享:1.文件描述符(打开文件的结构体),2.mmap建立的映射区(进程间通信详解)

特别的,fork之后父子进程谁先执行,取决于内核所使用的调度算法。

exec 函数族

当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。

//set follow-fork-mode child

//set follow-fork-mode parent

  • **int execl(const char path, const char arg, …) //用户自定义

    • 加载一个进程,通过 路径+程序名 来加载。
  • **int execlp(const char file, const char arg, …) //系统可执行程序

    • 加载一个程序,借助 PATH 环境变量

    • 参数 1: 要加载的程序的名字。该函数需要配合 PATH 环境变量来使用,当 PATH 中所有目录搜索后没有参数 1 则出错返回。

    • 该函数通常用来调用系统程序。如: ls、date、cp、cat 等命令

    • 练习:fork 一个子进程让其执行 ls 命令

      • #include <cstdlib>
        #include <cstdio>
        #include <unistd.h>
        #include <iostream>
        using namespace std;
        int main(void)
        {
            pid_t pid;
            pid = fork();
            if(pid == -1) {
                perror("fork error");
                exit(1);
            } else if(pid > 0) {
                cout << "parent" << endl;
            } else {
                execlp("ls", "ls", "-l", "-a", NULL);
            }
            return 0;
        }
        
  • **int execle(const char *path, const char arg, …, char const envp[])

  • **int execv(const char path, char const argv[])

  • **int execvp(const char file, char const argv[])

  • **int execve(const char *path, char const argv[], char const envp[]);

练习:将当前系统中进程的信息,打印到文件中。

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdlib>
#include <cstdio>
using namespace std;
int main(void)
{
    int fd = open("ps.out", O_WRONLY | O_CREAT, 0644);
    if(fd == -1) {
        perror("fd error");
        exit(1);
    }
    dup2(fd, STDOUT_FILENO);
    execlp("ps", "ps", "aux", NULL);//调用错误 -1,没有成功返回值
    perror("exec error");
    exit(1);
    close(fd);
    return 0;
}

exec 函数族一般规律

exec 函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值 -1。

  • l (list) 命令行参数列表
  • p (path) 搜索 file 时使用 path 变量
  • v (vector) 命令行参数数组
  • e (environment) 使用环境变量数组,不使用进程原有的环境变量
  • 事实上,只有 execve 是真正的系统调用,其他五个函数最终都调用 execve。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值