并发——进程(✔)

一. 程序

程序 (program)是什么?
        计算机程序 (computer program)一般是指以某些程序设计语言编程,能够运行于某种目标体系结构上

        程序 = 数据结构 + 算法

        数据结构:用来表示人们思维对象的抽象概念的物理表现叫做数据 (对问题中抽象出来的实体)

                对数据的处理规则叫做指令

        算法:解决问题完整而且准确的描述,是一系列解决问题的清晰的指令

        计算机程序就是算法和数据的集合 (算法操作数据)

        

        一个程序的执行过程就是一个计算

        程序是一个静态的概念

二. 程序的执行方式

(1) 顺序执行

        一个程序完全执行完毕后,另一个程序才能被执行

        

        缺点:CPU的利用率非常低

                   某些程序在等待外部条件的时候,CPU是空闲的

                        输入数据----->计算------>输出

(2) 并发执行

        多个程序同时运行 (宏观),本质上还是顺序执行

        一个程序有非常多的指令

                把一条指令的执行过程,分为几个不同的步骤:

                        取指令----->执行------>回写

                        不同的步骤,由不同的硬件去完成

                        

                        理论上来说,就可以多个程序同时运行 (单cpu,宏观)

 

为了提高cpu的利用率,增加吞吐量,引入"并发执行"

现代的操作系统为了能够让程序并发执行,特地引入"进程"的概念

        正在进行的程序

三. 进程

进程是什么?

        进程是具有独立功能的程序关于某一个数据集合上的一次运行活动

        

        理解为"炒菜 (进程)和菜谱 (程序)"的关系

        

        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  // 产生一个进程

./a.out &   进程在后台运行 (不会影响前台)

前台:shell终端就是前台,在shell终端上运行的进程 (会影响终端),可以称之为前台进程/交互进程


在linux中是如何描述进程的—进程控制块 (PCB):有一个进程就会对应唯一的进程控制块

struct task_strcut
    文件系统       fs      *root 用户  *pwd 工作目录
    进程文件表项   files   进程打开的文件
    进程地址空间   mm
    进程号         pid     标识一个进程  
    进程状态              运行、休眠...
    优先级                进程的调度方式
    信号处理方式   signal
    ...

在Linux中是如何管理进程控制块的:

        1. 链表:方便进程控制块的插入和删除

                ps  aux     列举所有进程的信息

                ps aux | grep a.out

                S+:S     sleep ---> 阻塞态       + ---> 前台

                R:running ---> 运行态 

        

        2.:方便找出谁创建了这个进程,方便找出某个进程的父进程和子进程

                pstree

        3. 哈希表 (数组):方便查找特定的进程  pid

                 kill -9 pid        杀死进程号为pid的进程

                 kill:发送一个信号    -9:信号值    pid:进程号

/proc 目录下  以文件的方式列举出了所有进程信息   ls /proc

四. 进程和程序的区别

(1)  程序是静态概念 (是指令和数据的集合)

       进程是动态的概念 (动态产生,动态消亡)

(2)  进程是一个程序的一次执行活动,一个程序可以对应多个进程 (多进程编程,多个进程的代码段是一样的,共享代码段的空间 (指令))


(3)  进程是一个独立的活动单位,进程是竞争系统资源的基本单位 (内存, cpu时间....)


OS为什么要引入进程呢?

        就是为了能够让程序并发运行(同一时间段有多个程序正在运行----宏观)

        程序的并发,实际上就是进程的并发


并发:在计算机科学领域中,是指同时执行多个独立的任务或者操作的能力,描述的是不同的任务交替 (时间短暂)获得CPU,从而达到同时执行的目的 (宏观),微观上并发仍然是顺序执行

并行:描述的是不同的任务同时使用CPU,微观、宏观统一,真正意义上的同时执行

五. 进程状态

OS把一个进程的执行过程,分为三个不同的阶段 (状态):

        就绪态 (Ready): 进程所有的准备工作已经完成,只需要CPU去执行进程的指令

        运行态 (Running): CPU正在执行这个进程的指令

        阻塞 (等待) 态 (Blocking,Waiting): 进程正在等待其他的外部事件 (如:输入......)

        停止态 (Stop): Ctrl + z 或者收到 STOP这个信号,进程会由 运行态---->停止态,输入fg,会由 停止态 --->就绪态

        僵尸态 (Zombie): 进程结束之后,短暂的一个状态,主要是让父进程收尸 (知道死因)

        

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

        

僵尸进程 (Zombie):

        当进程退出时,父进程没有读取到子进程的退出码,子进程就会成为僵尸进程

        一个进程结束了,但是它的父进程没有等待 (wait / waitpid)它,那么它就会变成僵尸进程

       僵尸进程会以终止状态保持在进程表中,一直等待父进程读取退出状态码

              

       危害:占用系统资源

        

孤儿进程:

        父进程结束了,子进程就被称为"孤儿进程" 

        孤儿进程被 init 系统进程收养

        

"就绪队列":Ready Queue

        所有处于就绪态的进程,都处于一个 "队列" 中

        

"调度程序":负责确定下一个进入"Running"状态的进程

        确定下一个占用CPU的进程

"调度策略":调度算法

        分时系统:调度策略以"时间片轮转"为主要策略的系统

                "时间片轮转":分时,每一个进程执行一段时间 (时间片)

                如:大部分的桌面操作系统,linux    android    windows    macos    unix......


        实时系统:调度策略以"实时策略"为主要策略的系统

                "实时策略":每一次调度都取优先级最高的那个进程执行,直到这个进程执行完毕或者它主动放弃CPU或者其他更高优先级的进程抢占
                如:ucos,freeRTOS...

                可抢占和不可抢占

六. linux进程地址空间布局

操作系统使得每个进程认为 它拥有所有资源 -------进程地址空间

程序运行的第一件事,就是申请一块内存区域来存储程序的"数据 (用户数据,指令)",不同的数据属性是不一样的,进程地址空间是进行分段管理 / 存储的

"分区域" 来存储程序的数据

"分段":分不同的逻辑区域

不同属性的数据,存储到不同的"内存段"中,不同的内存段的属性以及管理方法不同

.text  主要存放指令

         只读并且共享,这段内存在程序运行期间内不会被释放

         "指令段":随进程的持续性

.data   数据段

        主要存放程序已经初始化的全局变量和已经初始化的静态 (static)变量

        已经初始化的静态 (static)变量和已经初始化的全局变量一样,程序运行就立刻申请空间

        可读可写,这段内存在程序运行期间内不会被释放,随进程的持续性

.bss  数据段

        主要存放程序未初始化的全局变量和未初始化的静态 (static)变量,如果没有使用该变量,就不会申请空间,未初始化的全局变量和未初始化的静态 (static)变量暂时不需要空间

        可读可写,这段内存在程序运行期间内不会被释放,随进程的持续性

        .bss段,在程序初始化时,如果用到了这些变量可能全部被初始化为0 (未初始化的全局变量和未初始化的静态 (static)变量自动初始化为0)

.rodata  只读数据段

        主要存储程序中的只读数据 (如:字符串常量)

        只读 (不可以更改),这段内存在程序运行期间内不会被释放,随进程的持续性

栈 (stack)空间

        主要存放局部变量 (非static的局部变量)

        可读可写,这段空间,会自动释放 (代码块执行完了,代码块中的局部变量的空间就会自动释放),随代码块的持续性

        返回一个局部变量的地址,就会有问题

堆 (heap)空间:动态内存空间

        主要是malloc / realloc / colloc动态分配的空间

        可读可写的,这段内存在程序运行期间,一旦分配就会一直存在,直到你手动free或者程序结束

        防止''内存泄漏'' / "垃圾内存"

%p打印的是虚拟内存地址

#include <stdio.h>

int main() {
    static int a;
    printf("%p\n", &a);

    while (1);

    return 0;
}

gcc 1.c
./a.out &
./a.out &
./a.out &

三次打印的地址有没有可能相同?
    有可能,原因:虚拟内存地址

例子:

#include <stdio.h>
#include <stdlib.h>

int a = 5; // .data
int b; // .bss
static int k = 6; // .data

// f函数有问题,不能返回一个局部变量的地址
int* f() {
    int c = 5; // 栈空间
    return &c; // 返回一个局部变量的地址,c在返回后,就被系统回收了
}

// f1函数有问题,每调用一次,就会造成1024个字节的垃圾内存
void f1() {
    // p1是局部变量,但是p1指向的空间是堆空间
    char *p1 = malloc(1024);
}

// f2函数正确
char *f2() {
    char *p = "hello";
    return p; // 址传递,返回的是p所指向的地址
}

int main() {
    int *p = &a; // p在栈空间
    char *p1 = "12345"; // p1在栈空间
                        // "12345"在.rodata
    // *(p1 + 1) = 'B'; // 段错误,通过指针p1去修改只读内存
    // p1[1] = 'B'; // 同上  *(p1 + 1) <=====>p1[1]
    p1 = "ABC"; // 没问题,p1在栈空间,可读可写
    p1 = f2(); // p1指向"hello"

    char *p2 = malloc(1024); // p2在栈空间,1024个字节在堆空间
    *p2 = 'A'; // 没问题, 堆空间是可读可写的
    p2 = "nihao"; // 语法没问题,但是会造成内存泄漏,malloc开辟的1024个字节的空间找不到了

    char x[] = {"abcde"}; // x的空间在栈中,"abcde"在.rodata中,只是在x的空间中存在一份copy
    x[1] = 'B'; // 没问题
    // x = "xxxxx" // 有问题,数组名是一个指针常量,是一个指针类型的常量,不能修改

    return 0;
}

七. linux下关于进程的API函数解析

(1) 创建一个新进程 fork

        系统在实现fork时,有一个特点:写时复制 (copy-on-write)

        fork() 是用来创建一个新进程的,你要使用fork创建一个新进程,首先你要知道一个已有的进程里面包含什么东西?---------->  数据 (系统数据和用户数据) 和 指令

所以你创建一个新进程,也需要数据和指令,来自哪里呢?

        来源于父进程 (调用fork函数的进程,我们称之为父进程,新创建的进程称之为子进程),子进程的数据和指令都来自于父进程

        fork这个函数在创建子进程的时候:

                copy (克隆clone) 了父进程的数据和指令 !!!

                数据:用户数据和系统数据

                父进程的变量,数据对象

                标准IO的缓冲区

                文件描述符

                文件偏移量

                ......

        ======>

        fork 成功的时候,就会有两个一模一样的进程在执行一模一样的代码

        既然两个进程是一模一样的,如何区分父子进程呢?

                fork() 函数调用一次,有两次返回,在fork函数内部实现的。如果成功后,就会有两个进程在执行当前的代码,为了区分父子进程,所以fork一次调用,两次返回,一次是父进程返回,还有一次是子进程返回

fork的伪代码可能是这样实现的:

fork() {
    ......
    clone(克隆);
    ......
    // 下面的代码有两个进程在执行
    if (是父进程) {
        return 子进程的pid;
    } else if (是子进程){
        return 0;
    }
}

函数原型:

NAME
    fork - create a child process
    // 创建一个子进程
SYNOPSIS
    #include <unistd.h>

pid_t fork(void);

返回值:
    如果失败返回-1,同时errno被设置
    如果成功,有两个返回值(同一段代码有两个进程在运行)
    父进程返回子进程的进程号(pid)
    子进程返回0

        fork之后,操作系统会copy出一个与父进程完全相同的子进程 (申请一个进程控制块用于描述子进程),这两个进程共享代码段的空间 (指令),但是数据段是相互独立的,子进程的数据是从父进程地址空间中copy过来的,指令指针 (PC)也完全相同 (copy后,父进程运行到哪里,子进程也会运行到哪里)

        问题:

                fork 函数后面的语句,有两个进程正在执行,那么谁先执行 (单CPU)呢?

                        不一定,这个决定于进程调度算法


        copy完成之后,子进程就独立了 (有自己的pid),子代的父进程 ID与父代的进程 ID 相同


linux系统会为每一个进程,分配一个唯一的ID (>0的整数),使用类型pid_t (unsigned int)表示

还有两个配套的函数:用于获取进程自己的 ID 和父进程的 ID

NAME
    getpid,getppid - get process identification
SYNOPSIS
    #include <sys/types.h>
    #include <unistd.h>

pid_t getpid(void); // 用于获取进程本身的 ID

pid_t getppid(void); // 用于获取调用函数进程的父进程的 ID
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    int a = 1, b = 2;

    printf("hello world\n"); // 打印一次

    pid_t pid = fork();
    // 创建子进程,下面的代码就有两个进程在运行
    if (pid == -1) {
        perror("fork failed");
        return -1;
    } else if (pid > 0) {
        // 父进程,返回子进程的pid
        printf("I am father, a = %d, b = %d\n", a, b);
        printf("my son pid is %u\n", pid);
    } else if (pid == 0) {
        // 子进程,返回0
        printf("I am son, a = %d, b = %d\n", a, b);
        printf("my pid is %u\n", getpid());
        printf("my father pid is %u\n", getppid());
    }
    printf("haha nihao\n"); // 打印两次

    return 0;
}

hello world
I am father, a = 1, b = 2
my son pid is 41938
haha nihao
I am son, a = 1, b = 2
my pid is 41938
my father pid is 41937
haha nihao
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    int a = 1, b = 2;

    printf("hello world"); // 打印两次,仅仅是打印到缓冲区
                            // 缓冲区的数据会被copy到子进程

    pid_t pid = fork();
    // 创建子进程,下面的代码就有两个进程在运行
    if (pid == -1) {
        perror("fork failed");
        return -1;
    } else if (pid > 0) {
        // 父进程,返回子进程的pid
        printf("I am father, a = %d, b = %d\n", a, b);
        printf("my son pid is %u\n", pid);
        // while (1);
    } else if (pid == 0) {
        // 子进程,返回0
        printf("I am son, a = %d, b = %d\n", a, b);
        printf("my pid is %u\n", getpid());
        printf("my father pid is %u\n", getppid());
        // while (1);
    }
    printf("haha nihao\n"); // 打印两次

    return 0;
}

hello worldI am father, a = 1, b = 2
my son pid is 42046
haha nihao
hello worldI am son, a = 1, b = 2
my pid is 42046
my father pid is 1656
haha nihao
(2) 进程退出

进程退出有两种情况:

        一是自杀: 进程指令执行完毕或者自己终止了

                a. main函数执行完毕了

                        main函数退出,表明整个进程执行完毕

                b. 在进程执行的任意时刻调用

                        exit()  /  _exit()

NAME
    exit - cause normal process termination
    // 导致正常进程终止
SYNOPSIS
    #include <stdlib.h>

void exit(int status);

status:退出码,表示程序的退出状态,一般由程序员自己设定,返回给父进程的退出信息

正常退出,做一些清理工作(如:把缓冲区的东西,同步到文件中去)
NAME
    _exit  - terminate the calling process
SYNOPSIS
    #include <unistd.h>

void _exit(int status);

status:退出码,表示程序的退出状态,一般由程序员自己设定,返回给父进程的退出信息

和 exit 的区别是,直接终止进程,不会做清理工作

 return 和 exit  /  _exit 的区别?

        return 是函数返回,如果返回的是主函数,则会退出程序

        exit 是系统调用,在调用的地方强行退出程序,一旦运行进程就结束了

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

void f() {
    exit(1); // 同步缓冲区的内容
    // _exit(1); // 不会同步缓冲区的内容
}

int main() {
    printf("hello"); // 输出到标准IO的缓冲区
    f();
    printf("nihao\n");
    return 0;
}

        二是他杀:

                被操作系统干掉了 (内存的非法访问)

                被其他进程终止了 (进程间通信)

        

父进程可以通过调用wait / waitpid 函数得到子进程的退出码

(3) 等待子进程退出
NAME
    wait, waitpid, waitid - wait  for process to change state

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

pid_t wait(int *wstatus);
    
wstatus:用来保存被收集的进程的退出状态的(exit的退出码),是一个int类型的指                    
         针,如果我们对子进程的退出码没有任何兴趣,只想回收资源,避免子进程变为 
         僵尸进程,可以把wstatus设置为NULL

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

这两个函数的作用是用来等待某(些)一个子进程的状态发生改变的,等待的状态有三种:
    1. 子进程退出:main函数返回 / exit / _exit
    2. 子进程被信号终止
    3. 子进程被信号唤醒

进程一旦调用了wait,就立即阻塞自己,由wait自动分析当前进程的某个子进程是否退出。
如果找到这样一个子进程(僵尸进程),wait就会收集这个子进程的信息,
并且回收子进程的资源,如果没有子进程退出,则一直阻塞自己,
直到出现一个子进程的状态改变

父进程调用wait可以读取子进程的退出状态并且释放子进程的资源,假如没有调用wait,
那么子进程退出后,就会变成僵尸进程(zombie)
wait用来等待任意一个子进程的状态改变
pid_t wait(int *wstatus);

    wstatus:用来保存子进程的退出信息(子进程的退出码)
    
    返回值:
        成功返回退出的那个子进程的ID
        失败返回-1,同时errno被设置

wstatus保存了子进程的退出状态信息,这些状态信息都保存在一个整数中(status指向的内
存),可以使用一些宏解析那个整数:
常见的有:
    WIFEXITED(wstatus):wait if exited(exit,_exit)
        这个宏用来指出子进程是否为正常退出的,如果是正常退出的(exit,_exit,主函
数中的return),宏会返回一个非0值

    WEXITSTATUS(wstatus):wait exit status
        当WIFEXIFED(wstatus)返回非0时,我们可以使用这个宏去提取子进程的返回值
(子进程的退出码,如:子进程exit(5)退出,WIFEXITED(wstatus)就会返回5),如果进程
不是正常退出的(如:他杀),WIFEXIFED(wstatus)返回0时,这个值就没有任何意义

利用wstatus中的某一些位去存储一些固定的信息
退出码:unsigned char

    WIFSIGNALED(wstatus):wait if signaled
        如果进程是被信号终止的,这个宏就为真
waitpid是用来等待子进程状态改变的,只不过可以指定等待的子进程的进程号

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

pid:指定要等待的进程或者进程组
    pid == -1  表示等待任意子进程
    pid == 0  表示等待与调用进程同组的任意子进程
        "进程组":就是一组进程,默认每个进程都有属于自己的一个进程组(进程属性),
                  每一个进程组都有一个组长,创建这个进程组的进程就是组长进程,
                  进程组有一个组id,这个组id就是组长进程的id
    pid < -1  表示等待组id等于指定的pid绝对值的那个组的任意子进程(等待指定组中的
              一个子进程)
    pid > 0  表示等待指定的子进程(进程号为pid)

wstatus:同wait,保存子进程的退出码(指针,指向一个可用的内存)

options:选项
    0:表示堵塞等待
    WNOHANG:非阻塞等待,假设调用waitpid时没有子进程退出,立即返回

返回值:
    成功返回退出的那个进程的进程id
    失败返回-1,同时error被设置

wait(&wstatus) <======> waitpid(-1, &wstatus, 0)
(4) exec函数族

exec函数说明:
        fork是创建一个子进程,创建的子进程是父进程的副本,但是我们有时候想要让子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序的方法


        它可以根据指定的文件名找到可执行文件,并且使用它的数据段和指令段取代调用者进程的数据段和指令段。在执行完后,原调用者进程除了进程号以外,其他的全部都被替换为可执行程序的数据了


在linux中使用exec函数族主要有以下两种情况:

        a. 当进程认为自己不能再为系统和用户做出任何贡献的时候,就可以调用exec函数让自己 "重生"

        b. 如果一个进程想要执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用exec函数让子进程去执行另一个程序

exec函数就是让进程去执行指定的程序文件
你必须指定需要执行的程序的名字?
    一个在文件系统中的程序的文件名(绝对路径 / 相对路径)

让另一个程序文件的数据和指令覆盖当前进程的数据和指令

你指定的程序可能还需要参数,你必须指定程序文件的参数,在linux中,参数都是字符串
所以指定程序的参数有两种方式:
    l(exec中带l的版本):list
        把程序运行的参数,一个一个的列举出来,程序的第一个参数就是程序的名字(不需要
带路径的)
    "sum_test", "123", "456", NULL

    v(exec中带v的版本):vector 向量,数组
        把程序的运行参数,做成一个char*的数组(指针数组)
        char *argv[] = {"sum_test", "123", "456", NULL};

#include <stdio.h>

int my_atoi(char *str) {
    int d = 0;
    while (*str >= '0' && *str <= '9') {
        d = d * 10 + (*str - '0');
        str++;
    }
    return d;
}

int main(int argc, char *argv[]) {
    int a = my_atoi(argv[1]);
    int b = my_atoi(argv[2]);
    printf("sum = %d\n", a + b);
    return 0;
}
NAME
    execl,  execlp, execle, execv, execvp, execvpe - execute a file
    // 执行一个二进制文件
SYNOPSIS
    #include <unistd.h>

    extern char **environ;

int execl(const char *path, const char *arg, ... /* (char*) NULL */);
    // 把需要指向的程序通过list的方式指定

    path:指向要执行的那个程序文件的文件名(一般带路径)
        从第二个参数开始,就是执行该程序文件需要的命令行参数(要包括程序名,
程序名以不带路径的方式作为第一个参数),以NULL结尾

    返回值:
        失败返回-1,同时errno被设置
        成功,就永远不会返回了!!! 因为进程的整个指令和数据段都被替换掉了

    如:execl("/home/gec/sum_test", "sum_test", "250", "251", NULL);
--------------------------------------------------------------------------
// exec中带p的版本:p(PATH)
    系统中有一个环境变量,PATH
    环境变量是什么东西?
        就是整个系统环境(所有进程都共享)里面的变量
        echo $PATH
        PATH=dir1:dir2:dir3:......:dirN
        指定sh中程序文件的搜索路径,意思就是你运行一个程序时,不指定路径,系统就会到
这些目录中去找,如果找到了就执行程序,如果没找到就报错
        ------>
        如果你要运行的程序文件本身就在标准的搜索路径(PATH)下,就不需要指定路径了
        export可以导出一个临时的环境变量
        export PATH=$PATH:...
        
int execlp(const char *file, const char *arg, ... /* (char*) NULL */);
    file:你要执行的那个程序的名字(不需要带路径,因为已经在标准路径下面了),
其他参数和execl一样
-----------------------------------------------------------------------------
// v:vector 数组  使用数组的方式指定要执行的程序的参数
// char *argv[] = {"sum_test", "123", "456", NULL};
int execv(const char *path, char *const argv[]);
    path:指定要执行的那个程序文件的文件名(一般带路径)
    
    char *const argv[]:指针数组,本质是数组,里面的每一个元素是指针
    argv:指针数组,里面的每一个元素都是指针,指向程序需要的参数。执行该程序文件需
要的命令行参数(要包括程序名,程序名以不带路径的方式作为第一个成员),以NULL结尾

    返回值:
        失败返回-1,同时errno被设置
        成功,就永远不会返回了!!! 因为进程的整个指令和数据段都被替换掉了
-----------------------------------------------------------------------------
int execvp(const char *file, char *const argv[]);
    file:你要执行的那个程序的名字(不需要带路径,因为已经在标准路径下面了),
其他参数和execl一样

    char *argv[] = {"ps", "-aux", NULL};
    execvp("ps", argv);
-----------------------------------------------------------------------------
exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量(必须在标准的
搜索路径(PATH)下可以查找到),这里的"e"(environment)结尾的两个execle / execvpe
就可以在envp中指定当前进程所使用的环境变量,也可以在标准的搜索路径(PATH)下查找

int execle(const char *path, const char *arg, ...
                    /*, (char*) NULL, char *const envp[] */);

使用execvpe需要在头文件之前定义这个宏:#define _GNU_SOURCE
int execvpe(const char *file, char *const argv[], char *const envp[]);

    execlp / execvpe的其他参数都和以前的一样
    envp:新指定的环境变量,可以同时指定多个环境变量,以NULL结尾
例子:
    char *envp[] = {"PATH=/bin:/usr/bin", NULL};
    // path:相对路径 or 绝对路径
    execle("/bin/ps", "ps", "-aux", NULL, envp);

    char *argv[] = {"ps", "-aux", NULL};
    char *envp[] = {"PATH=/bin:/usr/bin", NULL};
    // file:可以不带路径
    execvpe("ps", argv, envp);

#include <stdio.h>

// 把指定的数字字符串转换为数字
int my_atoi(char *str) {
	int d = 0;
	while (*str >='0' && *str <= '9') {
		d = 10 * d + (*str - '0');
		str++;
	}
	return d;
}

int main(int argc,char *argv[]) {
	printf("i am sum_test\n");
	// 计算两个整数的和,加数都通过命令行参数指定
	int sum;
	int a, b;
	a = my_atoi(argv[1]);
	b = my_atoi(argv[2]);
	sum = a + b;
	printf("sum = %d\n", sum);
	return 250;
}
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main() {
	printf("hello\n");
	pid_t pid = fork(); // 创建子进程
	if (pid == -1) {
		perror("fork error");
		return -1;
	} else if (pid > 0) {
		// 父进程
		printf("i am father,my pid is %d,my son is %d\n", getpid(), pid);
		// 子进程结束,父进程没有读取子进程的退出码且回收资源,子进程变成僵尸进程
		int status; // 保存子进程的退出码
		pid_t p = wait(&status); // 阻塞自己
		// printf("status = %d\n", status); // 没有任何意义
		if (WIFEXITED(status)) { // 子进程是否是正常退出
			printf("son terminated normally\n");
			printf("son exit code: %d\n", WEXITSTATUS(status));
		} else {
			printf("son terminated not normally\n");
		}
		if (WIFSIGNALED(status)) {
			printf("son is kill by signal\n");
		}
	} else if (pid == 0) {
		// 子进程
		printf("i am son,my pid is %d,my father is %d\n", getpid(), getppid());
		// execl("/home/sc/sum_test", "sum_test", "250", "251", NULL);

		// char *argv[] = {"ps", "-aux", NULL};
        // execv("/bin/ps", argv);

        char *envp[] = {"PATH=/bin:/usr/bin", NULL};
        // path:相对路径 or 绝对路径
        execle("/bin/ps", "ps", "-aux", NULL, envp);

        // char *argv[] = {"ps", "-aux", NULL};
        // char *envp[] = {"PATH=/bin:/usr/bin", NULL};
        // file:可以不带路径
        // execvpe("ps", argv, envp);
	}

	return 0;
}
(5) system命令
NAME
    system - execute a shell command
    // system 是用来执行command指定的命令或者程序的,不会覆盖当前进程的数据和指令

SYNOPSIS
    #include <stdlib.h>

    int system(const char *command);
    
    如:
        system("ls -l");
        system("/home/gec/sum_test 123 456");

system是调用/bin/bash来执行参数指定的命令
带有阻塞功能,command指定的命令或者程序没有执行完,就不会执行system函数下面的内容
阻塞的原因:system函数会创建进程去执行指定的程序,必须给创建的进程收尸
不收尸:内存泄漏

八. 练习

1. 假设下面的程序编译后的名字是main,请问这个程序执行后,系统总共会出现多少个main进程?                             20个

int main() {
    fork();
    fork() && fork() || fork();
    fork();
}

2. 请问下面的程序执行结果为?

int main() {
    int i;
    for (i = 0; i < 2; i++) {
        fork();
        printf("_\n");
    }
    return 0;
}

_
_
_
_
_
_

3. 请问下面的程序执行结果为?

int main() {
    int i;
    for (i = 0; i < 2; i++) {
        fork();
        printf("_ "); // 输出到缓冲区
    }
    return 0;
}

_ _ _ _ _ _ _ _

4. 请问如下的程序中输出内容是什么?

int main() {
    fork() || fork();
    printf("+");
    return 0;
}

// +++



int main() {
    fork() && fork();
    printf("+");
    return 0;
}

// +++


int main() {  
    fork() || fork() || fork();  
    printf("+");  
    return 0;  
}
  
// ++++

5. 请问如下程序会生成多少个进程?

int main() {
    for (int i = 0; i < 5; i++) {
        int pid = fork();
    }
    return 0;
}

// 32 个进程
  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值