Skip to content

C语言控制进程

system函数

system 函数可以执行 shell 命令,例如我们编译一个 hello.c 代码为 hello 可执行程序,在终端要使用 ./hello 进行执行,使用 system 函数可以代替我们执行这个操作

hello.c

c
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello!\n");

    return 0;
}
shell
gcc hello.c -o hello
./hello
Hello!

system.c

c
#include <stdlib.h>

int main(int argc, char *argv[]) {
    system("./hello");

    return 0;
}
shell
gcc system.c -o system
./system 
Hello!

还可以使用 system 函数执行其他语言编写的可执行程序 例如有如下 python 代码文件

hello.py

python
print("Hello Python!")

可以在终端下使用命令python3 hello.py运行该 Python 脚本,那么就可以使用 system 函数代替我们执行

system.c

c
#include <stdlib.h>

int main(int argc, char *argv[]) {
 // system("./hello");
 system("python3 hello.py");

 return 0;
}

system函数的缺陷

假设我们现在有一个 sleep.c 程序,该程序会执行 10 秒,然后输出一句话。使用 system 函数执行该程序。

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

int main(int argc, char *argv[]){
    printf("sleep start\n");
    sleep(10);
    printf("sleep done\n");

    return 0;
}

system 函数执行程序会创建一个 sh -c xxx 的进程,该进程才会去执行我们要运行的 shell 命令,也就是说 system 函数会创建两个子进程。这样运行程序的消耗太大了。为了减小这种不必要的系统开销我们可以使用 fork + exec 函数族创建进程后再执行所需运行的程序。

fork函数创建进程

函数作用

  • 创建子进程

函数原型

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

pid_t fork(void);

函数参数

函数返回值

  • 调用成功:父进程返回子进程的PID,子进程返回0
  • 调用失败:返回-1,设置errno值。

fork函数代码片段实例

调用fork函数的内核实现原理

fork函数总结

  • fork函数的返回值?
    • 父进程返回子进程的PID,是一个大于0数;
    • 子进程返回0;
    • 特别需要注意的是:不是fork函数在一个进程中返回2个值,而是在父子进程各自返回一个值。
  • 子进程创建成功后,代码的执行位置?
    • 父进程执行到什么位置,子进程就从哪里执行
  • 如何区分父子进程
    • 通过fork函数的返回值
  • 父子进程的执行顺序
    • 不一定,哪个进程先抢到CPU,哪个进程就先执行

使用 fork 创建的进程与父进程之间的关系是 被克隆和克隆之间的关系。

子进程拥有父进程的所有资源,如果不使用 fork 函数的返回值进行区分,无法对两个进程进行精细的控制。

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

int main(int argc, char *argv[]){
    pid_t pid = fork();

    if (pid> 0) {
        printf("This is parent, pid = %d, ppid = %d\n", getpid(), getppid());
    }if(pid==0){
        printf("This is child, pid = %d, ppid = %d\n", getpid(), getppid());
    }

    return 0;
}

程序输出:

shell
This is parent, pid = 38499, ppid = 37251
This is child, pid = 38500, ppid = 38499

fork 实现的底层原理

  1. fork 会拷贝一份 task_struct
  2. 子进程修改一些必要的数据
  3. 加入到就绪队列

但是在第一步开始执行到第二步执行完之前,进程会处于一种不可抢占的状态就是不能够被其他信号中断且不可被其他程序时间片抢占硬件系统资源,直到第二步完成

系统调用的实现方式

首先我们要了解 CPU 的状态在 Linux 系统当中分为两种,一种是用户态,一种是内核态

  • 用户态只能执行部分指令
  • 内核态可以执行所有指令。当要操作硬件、内存中的内核态区域时必须使用 CPU 的内核态去完成。

另外需要了解的一个概念就是中断,可以简单的理解为硬件发生了一些事情会触发中断,当中断触发的时间 CPU 会从用户态转变为内核态去处理硬件发生的事情,处理完成之后再次切换到中断发生之前用户态执行到的位置继续完成用户态之前正在进行的任务。

fork 的性能

Tips

也就是说fork出来的子进程和父进程在虚拟内存空间上是完全一样的,并且只有在出现了写操作时才重新分配物理空间,但是变量的虚拟内存地址还是不变的(对应的物理内存却不一样了),子进程会拷贝一份父进程的物理内存空间,然后再进行修改。

如果没有写操作,那么子进程和父进程的物理内存空间是共享的。

fork 的拷贝

在逻辑上,父子进程的用户态空间(栈、堆、数据段)是拷贝的

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

int globalA = 10;

int main(int argc, char *argv[]){
    int stackA = 20;
    int *HeapA = (int *) malloc(sizeof(int));
    *HeapA = 30;

    if(fork() == 0){
        printf("This is child, globalA=%d, stackA=%d, heapA=%d.\n", globalA, stackA, *HeapA);
        ++globalA;
        ++stackA;
        ++*HeapA;
        printf("This is child, globalA=%d, stackA=%d, heapA=%d.\n", globalA, stackA, *HeapA);
    }else{
        sleep(1);
        printf("This is parent, globalA=%d, stackA=%d, heapA=%d.\n", globalA, stackA, *HeapA);
    }

    return 0;
}

程序输出:

shell
This is child, globalA=10, stackA=20, heapA=30.
This is child, globalA=11, stackA=21, heapA=31.
This is parent, globalA=10, stackA=20, heapA=30.

fork 函数作用下文件流的拷贝

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

int main(int argc, char *argv[]) {
#if 1
printf("hello\n");
#else
printf("hello");
#endif
    fork();
printf("world!\n");

    return 0;
}

在 hello 后面有换行符的时间只输出了一个 hello,没有换行符的时间却输出了两个 hello。

有换行符的时间 hello 在用户态的 FILE 文件缓冲区中已经被刷新到内核态的 stdout 文件中去了,fork 的时间子进程在 FILE 文件缓冲区中什么也没有拷贝到;没有换行符的时间 hello 仍然残留在用户态的 FILE 文件缓冲区中,fork 的时间子进程在 FILE 文件缓冲区中拷贝了一份 hello。

既然父子进程的用户态空间(栈、堆、数据段)是拷贝的,那内核态空间是拷贝的还是共享的呢?

答案是部分拷贝部分共享,其中共享的包含文件对象

c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int fd = open("file2", O_RDWR|O_CREAT, 0755);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    if (fork() == 0) {
        write(fd, "hello", 5);
    } else {
        sleep(1);
        write(fd, "world\n", 6);
    }

    return 0;
}

练习题

  • 编写程序,循环创建多个子进程,要求如下:
  1. 多个子进程是兄弟关系。
  2. 判断子进程是第几个子进程

画图讲解创建多个子进程遇到的问题

注意:若让多个子进程都是兄弟进程,必须保证 不能让子进程再去创建新的子进程。

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

int main(int argc, char *argv[]) {
    for (int i = 0; i < 3; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork error");
            return -1;
        }
        else if (pid > 0) {
            printf("father,pid is [%d]\n", getpid());
        }
        else if (pid == 0) {
            switch (i) {
            case 0:
                printf("first ");
                break;
            case 1:
                printf("second ");
                break;
            case 2:
                printf("thired ");
                break;
            default:
                break;
            }
            printf("child, pid id [%d], and father pid is [%d]\n", getpid(), getppid());
            break;
        }
    }

    return 0;
}
shell
father,pid is [800023]
first child, pid id [800025], and father pid is [800023]
second child, pid id [800026], and father pid is [800023]
father,pid is [800023]
thired child, pid id [800027], and father pid is [800023]
father,pid is [800023]
  • 编写程序,测试父子进程是否能够共享全局变量
cpp
//fork函数测试
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int g_var = 99;

int main(int argc, char *argv[]) {
    //创建子进程
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return -1;
    }
    else if (pid > 0) {
        printf("father: [%d], pid==[%d], fpid==[%d]\n", pid, getpid(), getppid());
        g_var++;
        printf("[%p]\t", &g_var);
        printf("father: g_var==[%d]\n", g_var);
    }
    else if (pid == 0) {
        printf("child: pid==[%d], fpid==[%d]\n", getpid(), getppid());
        sleep(1);//为了避免父进程还没有执行, 子进程已经结束了
        printf("[%p]\t", &g_var);
        printf("child: g_var==[%d]\n", g_var);
    }

    return 0;
}
shell
father: [239393], pid==[239386], fpid==[239379]                                                                                   
[0x555555558058]        father: g_var==[100]                                                                                      
child: pid==[239393], fpid==[239386]                                    
[0x555555558058]        child: g_var==[99]

重点通过这个案例讲解 读时共享,写时复制

父子进程不能共享全局变量,如果父子进程只是对全局变量做读操作,则父子进程在内存中只有一份,属于共享;但是如果父子进程对全局变量做写操作,会在内存中拷贝一个副本,然后在这个副本上进行修改,修改完成以后映射回去,属于复制。——读时共享,写时复制。

exec函数族

man execl

exec 函数族有很多函数,目前只需掌握 execl、execv 即可,其他都是扩展函数。l 是 list,v 是 vector。 execl 是可变参数的函数,execv 是参数元素是指针的数组。

函数作用

有的时候需要在一个进程里面执行其他的命令或者是用户自定义的应用程序,此时就用到了exec函数族当中的函数。

exec 函数可以将一个可执行程序文件加载到本进程的地址空间,执行 exec 函数会清空本进程空间的数据(栈、堆、数据段、代码段),将参数中 pathname 中可执行程序加载进来 取代原代码段 重置 pc 指针。

c
#include <unistd.h>

extern char** environ;

int execl(const char* pathname, const char* arg, .../* (char  *) NULL */);
int execlp(const char* file, const char* arg, .../* (char  *) NULL */);
int execle(const char* pathname, const char* arg, .../*, (char *) NULL, char *const envp[] */);
int execv(const char* pathname, char* const argv[]);
int execvp(const char* file, char* const argv[]);
int execvpe(const char* file, char* const argv[],char* const envp[]);

使用方法一般都是在父进程里面调用fork创建出子进程,然后在子进程里面调用exec函数。

exec 函数族的 6 个函数看起来似乎很复杂,但实际上无论是作用还是用法都非常相似,只有很微小的差别。

  • l(list):参数地址列表,以空指针结尾。
  • v(vector):存有各参数地址的指针数组的地址。
  • p(path):按 PATH 环境变量指定的目录搜索可执行文件。
  • e(environment):存有环境变量字符串地址的指针数组的地址。

参考资料 进程替换:exec 函数族

原理介绍

exec族函数的实现原理图:

如:execlp(“ls”, “ls”, “-l”, NULL);

总结:

exec函数是用一个新程序替换了当前进程的代码段、数据段、堆和栈;原有的进程空间没有发生变化,并没有创建新的进程,进程PID没有发生变化。

示例

  • 使用execl函数执行一个用户自定义的应用程序
cpp
#include <stdio.h>

int main(int argc, char *argv[]) 
{
    printf("Hello! The application is for execl test.\n");
    return 0;
}
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    printf("execl start\n");
    execl("./other", "", NULL);
    printf("execl over\n");

    return 0;
}
bash
execl start
Hello! The application is for execl test.

使用execlp函数执行一个linux系统命令

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

int main(int argc, char *argv[]) {
    //vs code自动编译会卡,无法执行
    //需要自己手动gcc编译执行
    execl("/usr/bin/pwd", "pwd", NULL);
    perror("execl error");

    return 0;
}
bash
gcc -o execl execl.c 
./execl
/home/ubuntu/Code/Linux

注意

当execl和execlp函数执行成功后,不返回,并且不会执行execl后面的代码逻辑,原因是调用execl函数成功以后,exec函数指定的代码段已经将原有的代码段替换了。

通过执行结果可以看到第一个execl语句执行以后,后面的代码并没有得到执行

进程回收

为什么要进行进程资源的回收

当一个进程退出之后,进程能够回收自己的用户区的资源,但是不能回收内核空间的PCB资源,必须由它的父进程调用wait或者waitpid函数完成对子进程的回收,避免造成系统资源的浪费。

孤儿进程

孤儿进程的概念

若子进程的父进程已经死掉,而子进程还存活着,这个进程就成了孤儿进程。

  • 为了保证每个进程都有一个父进程,孤儿进程会被init进程领养,init进程成为了孤儿进程的养父进程,当孤儿进程退出之后,由init进程完成对孤儿进程的回收。
  • 模拟孤儿进程的案例

编写模拟孤儿进程的代码讲解孤儿进程,验证孤儿进程的父进程是否由原来的父进程变成了init进程。

僵尸进程

僵尸进程的概念

若子进程死了,父进程还活着, 但是父进程没有调用wait或waitpid函数完成对子进程的回收,则该子进程就成了僵尸进程。

  • 如何解决僵尸进程
    • 由于僵尸进程是一个已经死亡的进程,所以不能使用kill命令将其杀死
    • 通过杀死其父进程的方法可以消除僵尸进程。
    • 杀死其父进程后,这个僵尸进程会被init进程领养,由init进程完成对僵尸进程的回收。
  • 模拟僵尸进程的案例

编写模拟僵尸进程的代码讲解僵尸进程,验证若子进程先于父进程退出,而父进程没有调用wait或者waitpid函数进行回收,从而使子进程成为了僵尸进程。

进程回收函数

wait函数

  • 函数原型
cpp
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* wstatus);
int waitid(idtype_t idtype, id_t id, siginfo_t* infop, int options);

函数作用

  • 阻塞并等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)。

函数返回值

  • 成功:清理掉的子进程ID;
  • 失败:-1 (没有子进程)

函数参数

  • status参数:子进程的退出状态,传出参数
    • WIFEXITED(status):为非0 → 进程正常结束
    • WEXITSTATUS(status):获取进程退出状态
    • WIFSIGNALED(status):为非0 → 进程异常终止
    • WTERMSIG(status):取得进程终止的信号编号。
练习

使用wait函数完成父进程对子进程的回收

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

int main(int argc, char *argv[]) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return -1;
    }
    else if (pid > 0) {
        printf("This is father, pid [%d]\n", getpid());

        int status;
        pid_t wpid = wait(&status);
        printf("child is closed, pid is %d\n", wpid);
        if (WIFEXITED(status)) {
            printf("正常退出,退出代码%d\n", WEXITSTATUS(status));
        }
        else if (WIFSIGNALED(status)) {
            printf("被信号中止程序,退出代码%d\n", WTERMSIG(status));
        }
        // 使父进程休眠15s,会导致子进程完成业务后先退出程序变成僵尸进程
        // sleep(15);
    }
    else {
        printf("This is child, pid [%d]\n", getpid());
        sleep(20);
    }

    return 0;
}
bash
This is child, pid [810858]
This is father, pid [810856]
child is closed, pid is 810858
正常退出,退出代码0
kill -9 810463
This is father, pid [810461]
This is child, pid [810463]
child is closed, pid is 810463
被信号中止程序,退出代码9

waitpid函数

  • 函数原型
cpp
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int* wstatus, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t* infop, int options);

函数作用

同wait函数

函数参数

pid-1 等待任一子进程。与wait等效。
>0 等待其进程ID与pid相等的子进程。
= 0 等待进程组ID与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
<-1 等待其组ID等于pid的绝对值的任一子进程。(适用于子进程在其他组的情况)
status子进程的退出状态,用法同wait函数。
options设置为WNOHANG,函数非阻塞,设置为0,函数阻塞。

函数返回值

  • >0:返回回收掉的子进程ID;
  • -1:无子进程
  • =0:参3为WNOHANG,且子进程正在运行。
练习

使用waitpid函数完成对子进程的回收

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

int main(int argc, char *argv[]) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return -1;
    }
    else if (pid > 0) {
        printf("This is father, pid [%d]\n", getpid());
        int status;
        pid_t wpid = waitpid(-1, &status, 0);
        if (WIFEXITED(status)) {
            printf("[%d]正常退出,退出代码%d\n", wpid, WEXITSTATUS(status));
        }
        else if (WTERMSIG(status)) {
            printf("[%d]被信号终止程序,退出代码%d\n", wpid, WTERMSIG(status));
        }
    }
    else {
        printf("This is child, pid [%d]\n", getpid());
        sleep(10);
    }

    return 0;
}
This is father, pid [812868]
This is child, pid [812870]
[812870]正常退出,退出代码0

进程的终止

在 Linux 系统中 echo $? 可以查看上一个进程退出的状态。连着使用两次 echo $? ,第一次正常看到上一个程序的退出状态,第二次必定是 0,因为第二次看到的是 echo $? 的退出状态。

正常终止

  • 在 main 函数中调用 return
  • 在任何位置调用 exit();_exit();_Exit();
    • exit(); 会刷新 stdout 缓冲区
    • _exit();_Exit(); 不会刷新 stdout 缓冲区

异常终止

  • 主动异常终止,调用abort()
  • 被动异常终止,被另一个进程/硬件发信号终止。如kill -9 xxx

可以使用 kill -l 查看 Linux 系统中的信号

守护进程

Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字,如vsftpd

Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互。不受用户登录、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现;ftp服务器;nfs服务器等。

总结守护进程的特点

  • Linux后台服务进程
  • 独立于控制终端
  • 周期性的执行某种任务
  • 不受用户登陆和注销的影响
  • 一般采用以d结尾的名字

进程组和会话

进程组

  • 进程组是一个或者多个进程的集合,每个进程都属于一个进程组,引入进程组是为了简化对进程的管理。当父进程创建子进程的时候,默认子进程与父进程属于同一个进程组。

进程组ID==第一个进程ID(组长进程)。如父进程创建了多个子进程,父进程和多个子进程同属于一个组,而由于父进程是进程组里的第一个进程,所以父进程就是这个组的组长,组长ID==父进程ID。

  • 可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死。
  • 只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
  • 进程组生存期:从进程组创建到最后一个进程离开

会话

  • 一个会话是一个或多个进程组的集合。
  • 创建会话的进程不能是进程组组长
  • 创建会话的进程成为一个进程组的组长进程,同时也成为会话的会长。
  • 需要有root权限(ubuntu不需要)
  • 新创建的会话丢弃原有的控制终端
  • 建立新会话时,先调用fork,父进程终止,子进程调用setsid函数
  • 可以使用ps ajx来查看进程组ID和会话ID
  • 可以fork出几个子进程,然后查看进程组ID和会话ID
  • 进程组和会话的关系图

创建守护进程的模型

第1步:fork子进程,父进程退出

  • 子进程继承了父进程的进程组ID,但具有一个新的进程ID,这样就保证了子进程不是一个进程组的组长ID,这对于下面要做的setsid函数的调用是必要的前提条件

第2步:子进程调用setsid函数创建新会话

  • 调用这个函数以后
  • 该进程成为新会话的首进程,是会话的会长
  • 成为一个新进程组的组长进程,是进程组组长
  • 不受控制终端的影响

第3步:改变当前工作目录chdir

  • 如:a.out在U盘上,启动这个程序,这个程序的当前的工作目录就是这个u盘,如果u盘拔掉后进程的当前工作目录将消失,a.out将不能正常工作。

第4步:重设文件掩码 mode & ~umask

  • 子进程会继承父进程的掩码
  • 增加子进程程序操作的灵活性
  • umask(0000);

第5步:关闭文件描述符

  • 守护进程不受控制终端的影响所以可以关闭,以释放资源

    c
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

第6步:执行核心工作

  • 守护进程的核心代码逻辑

创建守护进程示例

cpp
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>

void Daemon();
void doSomething();

int main(int argc, char *argv[]) {
    Daemon();

    return 0;
}

void Daemon() {
    // 1.关闭父进程
    if (fork() != 0) {
        _exit(0);
    }

    // 2.创建新会话
    setsid();

    // 3.改变当前工作目录
    chdir("/home/morax/");

    // 4.重设文件掩码
    umask(0);

    // 5.关闭系统默认打开的文件描述符
    for (int i = 0; i < 2; ++i) {
        close(i);
    }

    // 6.执行守护进程的核心代码逻辑
    doSomething();
}

// 每隔2S钟获取一次系统时间,并将这个时间写入磁盘文件。
void doSomething() {
    int fd = open("file1", O_RDWR | O_CREAT, 0744);
    time_t now;
    struct tm *pTm;
    char buf[1024] = {0};

    for (int i = 0; i < 5; ++i) {
        now = time(NULL);
        pTm = localtime(&now);
        memset(buf, 0x0, sizeof(buf));
        sprintf(buf, "%4d%02d%02d%02d:%02d:%02d\n", pTm->tm_year + 1900, pTm->tm_mon + 1, pTm->tm_mday, pTm->tm_hour, pTm->tm_min, pTm->tm_sec);
        write(fd, buf, strlen(buf));
        sleep(2);
    }

    close(fd);
}

练习

编写一个守护进程,每隔2S钟获取一次系统时间,并将这个时间写入磁盘文件。

分析:首先要按照守护进行的步骤创建一个守护进程

题目要求每隔2S钟,所以需要一个定时器,2S钟触发一次,需要调用setitimer函数创建一个定时器,并且要捕获SIGALRM信号,然后在SIGALRM信号处理函数里面完成获取系统时间,然后将时间写入文件。

用到的主要知识点:

  • 创建守护进程的模型
  • setitimer函数的使用
  • sigaction函数
  • 文件I/O操作
  • 获取系统时间函数time,将time_t类型转换为字符串ctime函数

本作业的完成需要借助进程间通信的知识,学习完成后可进行完成。

cpp
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

// 创建守护进程
void deamon(void);

// 守护进程要做的事
void doWork(void);

// 自定义信号处理函数
void sigFunc(int sigNum);

int main(int argc, char *argv[]) {
    deamon();

    return 0;
}

void deamon(void) {
    if (fork() != 0) {
        _exit(0);
    }

    setsid();

    chdir("~/");

    umask(0);

    for (int i = 0; i < 2; ++i) {
        close(i);
    }

    doWork();
}

void doWork(void) {
    // 注册信号
#if 0
    // 方式一 使用signal注册信号
    signal(SIGALRM, sigFunc);
#else
    // 方式二 使用sigaction注册信号
    struct sigaction set;
    memset(&set, 0x0, sizeof(set));
    set.sa_handler = sigFunc;
    sigaction(SIGALRM, &set, NULL);
#endif

    // 设置定时器
    struct itimerval itimer;
    itimer.it_value.tv_sec = 2;
    itimer.it_value.tv_usec = 0;
    itimer.it_interval.tv_sec = 2;
    itimer.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &itimer, NULL);
    
    // 使进程一直运行
    while (1) {};
}

void sigFunc(int sigNum) {
    // 获取当前系统时间并转换为本地时间
    time_t now = time(NULL);
    struct tm *pTm = localtime(&now);

    // 将本地时间格式化后写入文件
    int fd = open("file1", O_WRONLY | O_CREAT | O_APPEND, 0744);
    char buf[1024] = {0};
    sprintf(buf, "%4d%02d%02d%02d:%02d:%02d\n", pTm->tm_year + 1900, pTm->tm_mon + 1, pTm->tm_mday, pTm->tm_hour, pTm->tm_min, pTm->tm_sec);
    write(fd, buf, strlen(buf) + 1);
    close(fd);
}

本章作业

测试父子进程之间是否共享文件

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

int main(int argc, char *argv[]) {
    int fd = open("./src/012_wait_test.txt", O_RDWR | O_CREAT, 0777);

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return -1;
    }
    else if (pid > 0) {
        printf("This is father, pid is [%d]\n", getpid());
        char content[] = "测试子进程能否成功读取该内容\n成功读取说明进程共享文件指针\n读取失败则不能共享文件指针";
        write(fd, content, sizeof(content));
        printf("父进程写入完成\n");

        int status;
        pid_t wpid = waitpid(-1, &status, 0);
        if (WIFEXITED(status)) {
            printf("子进程正常退出,退出代码%d\n", WEXITSTATUS(status));
        }
        else if (WIFSIGNALED(status)) {
            printf("子进程被信号终止,退出代码%d\n", WTERMSIG(status));
        }
    }
    else {
        printf("This is child, pid is [%d]\n", getpid());
        lseek(fd, 0, SEEK_SET);
        char buf[1024];
        read(fd, buf, sizeof(buf));
        printf("子进程读取完成,文件内容[%s]\n", buf);
    }

    return 0;
}
bash
This is father, pid is [821976]
父进程写入完成
This is child, pid is [821978]
子进程读取完成,文件内容[测试子进程能否成功读取该内容
成功读取说明进程共享文件指针
读取失败则不能共享文件指针]
子进程正常退出,退出代码0
ubuntu@VM-24-9-ubuntu:~/Code/Linux$

创建3个子进程并获取其退出状态

父进程fork三个子进程:

  • 其中一个调用ps命令;
  • 一个调用自定义应用程序;
  • 一个调用会出现段错误的程序。

段错误

  1. 访问了非法内存
  2. 访问了不可写的区域进行写操作
  3. 栈空间溢出
c
char* p = "hello,world";    
p[0]='a'

父进程回收三个子进程(waitpid),并且打印三个子进程的退出状态。

c
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello! The application is for execl test.\n");
    return 0;
}
c
#include <stdio.h>

int main(int argc, char *argv[]) {
    char* p = "hello word";
    p[0] = 'a';
    printf("%s\n", p);

    return 0;
}
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    for (int i = 0; i < 3; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork error");
            return -1;
        }
        else if (pid > 0) {
            printf("This is father, pid is [%d]\n", getpid());

            int status;
            int index = 0;
            pid_t wpid = 0;
            while (wpid != -1) {
                wpid = waitpid(-1, &status, 0);
                if (WIFEXITED(status)) {
                    printf("子进程[%d]正常退出,退出代码[%d]\n", wpid, WEXITSTATUS(status));
                }
                else if (WIFSIGNALED(status)) {
                    printf("子进程[%d]被信号终止,退出代码[%d]\n", wpid, WTERMSIG(status));
                }
                index++;
            }
        }
        else {
            switch (i) {
            case 0:
                printf("这是第1个子进程,进程pid[%d]\n", getpid());
                execlp("ps", "ps", "-ef", NULL);
                break;
            case 1:
                printf("这是第2个子进程,进程pid[%d]\n", getpid());
                execlp("./Linux/execl_test", "execl_test", NULL);
                break;
            case 2:
                printf("这是第3个子进程,进程pid[%d]\n", getpid());
                execlp("./Linux/wait_02_test", "wait_02_test", NULL);
                break;
            default:
                break;
            }
            break;
        }
    }

    return 0;
}
bash
这是第1个子进程,进程pid[829529]
This is father, pid is [829527]
UID          PID    PPID  C STIME TTY          TIME CMD
root           2       0  0 Mar07 ?        00:00:00 [kthreadd]
root           3       2  0 Mar07 ?        00:00:00 [rcu_gp]
root           4       2  0 Mar07 ?        00:00:00 [rcu_par_gp]
root           5       2  0 Mar07 ?        00:00:00 [netns]
... ...
ubuntu    829527  829520  0 10:52 ?        00:00:00 /home/ubuntu/Code/Build_files/wait_02.out
ubuntu    829529  829527  0 10:52 ?        00:00:00 ps -ef
子进程[829529]正常退出,退出代码[0]
子进程[-1]正常退出,退出代码[0]
This is father, pid is [829527]
这是第2个子进程,进程pid[829530]
Hello! The application is for execl test.
子进程[829530]正常退出,退出代码[0]
子进程[-1]正常退出,退出代码[0]
This is father, pid is [829527]
这是第3个子进程,进程pid[829531]
子进程[829531]被信号终止,退出代码[11]
子进程[-1]被信号终止,退出代码[11]