目录
- 准备工作
-
- [1. 编辑c语言源程序](#1. 编辑c语言源程序)
- [2. 编译c语言源程序](#2. 编译c语言源程序)
- [3. 调试运行](#3. 调试运行)
-
- [方式一:gdb ./test.c](#方式一:gdb ./test.c)
- 方式二:./test
- [gcc 编译器的安装](#gcc 编译器的安装)
- 一、关于进程的系统调用
- [二、 探究内容](#二、 探究内容)
准备工作
1. 编辑c语言源程序
$vim test.c
~
~
~
~
按键,进入编辑模式,程序输入完成后,按键,退出编辑模式。
按 : 键,进入命令行模式。所有需要在底部命令行执行的指令,都需要以:开头来触发。
- 按
:w
保存当前文件(write),但不退出vim。 - 按
:q
(quit)不存盘并退出vim编辑,返回命令行方式。(若文件有未保存的修改,执行:q
会报错) - 按
:wq
保存并退出,是最常用的命令。 - 按
:q!
强制退出,不保存任何修改。
2. 编译c语言源程序
$gcc -o test -g test.c
没有错误提示,表示编译成功;否则返回vim中进行修改。
编译成功后,可通过 ./test
运行程序,或通过 gdb ./test
启动调试工具进行调试。
参数说明
gcc
:GNU C 编译器,用于将 C 语言源代码编译为可执行程序-o test
:指定编译输出的可执行文件名为test
(如果不指定,默认输出文件名为a.out
)-g
:在编译时添加调试信息,方便后续对程序进行调试(如设置断点、查看变量等)。test.c
:要编译的 C 语言源代码文件。
3. 调试运行
方式一:gdb ./test.c
$ gdb test
:
:<出现提示信息>
:
(gdb) 输入run ,程序即可运行,输出运行结果。
方式二:./test
通过 ./test
直接运行程序,得出结果
gcc 编译器的安装
ubuntu上没有装gcc编译器,需要自己安装。
安装命令如下:
sudo apt install gcc
由于上一次在安装了vim(shell编程(2)------基础知识结合小案例实现),本次安装应该没有任何问题。
安装完成后,使用gcc --version
命令查看,证明安装成功。
一、关于进程的系统调用
- 进程的创建 fork()
格式:pid=fork()
功能:创建一个新进程,新进程与父进程具有相同的代码,父子进程都从fork()之后的那条语句开始执行。
- a)对于父进程,pid 的值>0;
- b)对于子进程,pid的值=0;
- c)创建失败,pid的值<0。
pid 是 Process ID(进程标识符) 的缩写,是操作系统为每个运行中的进程分配的唯一整数标识符,用于唯一标识一个进程。
-
进程的终止 exit()
格式:exit(status)
功能:终止当前进程的执行,status是一个整数,其值可以返回父进程。
-
进程的同步 wait()
格式:wait()
功能:使父进程进入睡眠状态,直到子进程终止后被唤醒,用于实现父子进程的同步。
-
进程的延迟 sleep()
格式:sleep(n)
功能:当前进程延迟n秒执行。
-
进程执行另一程序 execl()
功能:在当前进程中执行另一个程序,替换原有进程的代码、数据等,实现进程功能
二、 探究内容
1、进程的创建
执行下面的程序,分析执行的结果。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // 包含fork函数的声明
int main()
{ int i;
printf("just 1 process.\n");
i=fork();
if (i==0)
printf("I am child.\n");
else if (i>0)
printf("I am parent.\n");
else
printf("fork() failed.\n");
printf("program end.\n");
return 0;
}
分析
创建子进程:执行 i = fork()
fork() 会创建一个与父进程几乎完全相同的子进程,父子进程从 fork() 之后的代码开始分别执行。
父进程中,fork() 返回子进程的 PID(大于 0),因此 i > 0。
子进程中,fork() 返回 0,因此 i == 0。
若创建失败(极少发生),fork() 返回负数,执行 fork() failed.
结论
- fork() 调用后,系统中会出现两个独立的进程(父进程和子进程),且从 fork() 之后的代码开始并行执行。
- 父子进程通过 fork() 的返回值区分身份(父进程得到子进程 PID,子进程得到 0)。
- 父子进程的执行顺序由操作系统调度决定,因此分支代码的输出顺序可能不固定,但初始的 just 1 process. 只会输出一次(仅父进程在 fork() 前执行)。
运行结果
2、进程的同步
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<sys/wait.h> //wait()
int main()
{ int i;
printf("just 1 process.\n");
i=fork();
if (i>0)
{
printf("I am parent.\n");
wait();
}
else{
if (i==0)
{ printf("I am child.\n");
exit(1);
}
}
printf("program end.\n");
}
代码执行流程
- 初始阶段:
程序启动后,只有父进程运行,执行printf("just 1 process.\n")
,输出:just 1 process.
- 创建子进程:执行 i = fork()
- 父进程中,fork() 返回子进程的 PID(i > 0)。
- 子进程中,fork() 返回 0(i == 0)。
- 父进程分支(i > 0):
- 执行
printf("I am parent.\n")
,输出:I am parent.
- 调用 wait():父进程进入阻塞状态,等待子进程终止后再继续执行。
- 执行
- 子进程分支(i == 0):
- 执行
printf("I am child.\n")
,输出:I am child.
- 调用 exit(1):子进程立即终止,退出状态码为 1(该状态码会被父进程的 wait() 获取,但代码中未显式处理)
- 执行
- 后续执行:
子进程终止后,父进程的 wait() 被唤醒,继续执行后续代码。
父进程执行最后一句printf("program end.\n")
,输出:program end.
子进程因已调用 exit(1),不会执行最后一句 printf(直接终止)。
关键特性
- 同步机制:wait() 确保父进程等待子进程执行完毕后再继续,避免父进程先于子进程结束。
- 子进程终止:exit(1) 使子进程主动退出,且不再执行后续代码(因此子进程不会输出
program end.
)。 - 输出顺序固定:由于 wait() 的作用,父进程必须等待子进程输出
I am child.
并终止后,才会继续输出program end.
,因此输出顺序是确定的。
结果
- 使用wait()函数时,需要在开始加入
#include<sys/wait.h>
头文件,否则会发生以下报错。
2.wait 函数的声明是pid_t wait(int *stat_loc);
,它需要一个指向 int 的指针作为参数,用于接收子进程的退出状态信息。如果不需要获取子进程的退出状态,可以传递 NULL。
wait() 函数需要一个参数(用于存储子进程的退出状态)
这里根据需要,向wait()函数传入NULL就行了,我并不关注子进程的退出状态。
3.进程的延迟
c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h> //fork(),sleep()
int main(){
int i,j,k;
i=fork();
if(i>0)
for(j=1;j<=5;j++){
sleep(1);
printf("I am parent.\n");
}
else
if(i==0){
for(k=1;k<=5;k++){
sleep(1);
printf("I am child.\n");
}
}
return 0;
}
分析
通过fork()系统调用创建一个子进程,父进程i > 0,执行一个循环,从 j=1到 j=5,每次循环休眠1秒后输出 I am parent.。子进程i == 0,执行另一个循环,从 k=1到 k=5,每次循环休眠1秒后输出 I am child.。父子进程并发执行各自的循环逻辑,输出顺序由操作系统调度决定,交替输出"父进程"和"子进程"的提示信息,最终各自完成 5 次输出后结束。
结果
4.进程执行另一程序
c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
int i;
char name[20];
printf("Please input a directory name:\n");
scanf("%s",name);
i=fork(); //单独赋值,避免优先级问题
if(i==-1){
perror("fork failed");
return 1;
}else if(i==0){
execl("/bin/ls","ls","-l",name,NULL);
else{
}
}
结果
5.编写一段程序
要求:
编写一段程序,使用系统调用fork()创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程活动。让每一个进程在屏幕上显示一个字符:父进程显示字符"a";子进程分别显示字符"b"和字符"c"。试观察记录屏幕上的显示结果,并分析原因。
代码:
c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
// 创建第一个子进程
pid_t pid1 = fork();
if (pid1 == -1) {
perror("fork failed");
return 1;
}
if (pid1 == 0) {
// 第一个子进程:输出 'b'
printf("b");
return 0;
}
// 父进程继续创建第二个子进程
pid_t pid2 = fork();
if (pid2 == -1) {
perror("fork failed");
return 1;
}
if (pid2 == 0) {
// 第二个子进程:输出 'c'
printf("c");
return 0;
}
// 父进程:等待两个子进程结束后输出 'a'
wait(NULL); // 等待第一个子进程
wait(NULL); // 等待第二个子进程
printf("a");
return 0;
}
结果
原因分析
-
进程创建机制:
- 父进程首先通过
fork()
创建第一个子进程(输出 'b'),再创建第二个子进程(输出 'c')。 fork()
后,子进程与父进程并发执行,操作系统的进程调度算法决定了它们的执行顺序(内核级调度,不受用户程序直接控制)。
- 父进程首先通过
-
输出顺序不确定性:
- 三个进程(父进程、子进程1、子进程2)的执行顺序由 CPU 调度决定。例如:
- 可能子进程1先执行(输出 'b'),再子进程2(输出 'c'),最后父进程(输出 'a'),结果为
bca
; - 也可能子进程2先执行(输出 'c'),再子进程1(输出 'b'),最后父进程(输出 'a'),结果为
cba
。
- 可能子进程1先执行(输出 'b'),再子进程2(输出 'c'),最后父进程(输出 'a'),结果为
- 三个进程(父进程、子进程1、子进程2)的执行顺序由 CPU 调度决定。例如:
-
父进程的等待逻辑:
- 代码中父进程通过
wait(NULL)
等待两个子进程结束后再输出 'a',因此 'a' 一定在最后输出(避免父进程先于子进程执行)。但两个子进程之间的输出顺序仍不确定。
- 代码中父进程通过
总结
输出结果中 a
始终在最后,但 b
和 c
的顺序不固定,这是因为 子进程与父进程的执行顺序由操作系统调度算法决定,属于正常的并发执行特性。若需固定顺序,需通过进程同步机制(如信号量、管道)额外控制。