当一个进程调用fork函数时,它会创建一个新的进程,新的进程称为子进程,而原进程则称为父进程。在Linux系统中,fork函数是非常重要的函数之一。
fork函数的返回值有三种情况:
当控制转移到内核中的fork代码后,内核会执行以下操作:
需要注意的是,fork函数创建的子进程是父进程的副本,包括代码段、数据段、堆栈和文件描述符等。子进程与父进程共享同样的程序代码,但是每个进程都有自己的地址空间,因此它们可以独立地修改自己的变量而不会影响其他进程的变量。此外,fork函数的调用会导致进程表中的进程数增加,因此在一些系统上,fork函数的调用次数受到一些限制,以避免进程表溢出。
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方,但每个进程都不相互影响,如下代码:
1 #include <iostream>
2 #include <unistd.h>
5 using namespace std;
6 int main()
7 {
8 cout << "start" << endl;
9 pid_t ret = fork();
10 if(ret > 0)
11 {
12 //parents
13 cout << "hello parents" << endl;
14 }
15 else{
16 cout << "hello son" << endl;
17 }
18 cout << "end" << endl;
19 return 0;
20 }
运行结果:
我们可以看到start
只打印了一行,而end
打印了两行,这是因为在这个程序中,调用fork函数之后,父进程和子进程都会从fork函数返回,并继续执行后面的代码。因此,我们可以看到在父进程中,输出了"hello parents",而在子进程中,输出了"hello son"。同时,无论是父进程还是子进程都会执行最后一行输出"end"。这是因为fork函数创建了一个新的进程,而这个新的进程会继承父进程的代码和数据段,包括输出语句。因此,父子进程都会输出"end"。
注意,fork之后,谁先执行完全由调度器决定。
在fork函数创建子进程时,子进程会继承父进程的数据段,包括变量的值。在fork函数之后,父子进程都可以访问这些变量,并且它们的值是相同的。这种情况下,父子进程都可以读取这些变量,但是如果任意一方试图修改这些变量,就会发生写时拷贝。在这种情况下,操作系统会为子进程复制一份父进程的数据段,以保证父子进程的数据不会互相干扰。
需要注意的是,写时拷贝只会在修改变量时发生。如果父子进程只是读取变量,那么它们仍然共享相同的数据段,而不会发生写时拷贝。
总之,写时拷贝是一种非常有效的机制,它可以保证父子进程之间的数据独立性,同时避免了不必要的内存复制。
进程终止是指进程结束运行的过程。一个进程可以正常终止,也可以异常终止。正常终止是指进程按照预期执行完毕,退出运行的过程。异常终止是指进程在执行过程中遇到了错误,或者被强制终止,导致进程提前结束运行的过程。
进程正常终止的情况有:
进程异常终止的情况有:
_exit函数是一个系统调用,用于终止进程的运行,它不同于exit函数,不会执行一些清理操作,例如关闭文件、清理缓存等,而是直接终止进程。_exit函数的原型如下:
void _exit(int status);
其中,status参数表示进程的退出状态码,通常情况下,0表示进程正常退出,非0表示进程异常退出。在调用_exit函数之后,进程的所有打开文件都会被关闭,所有未写入的缓冲数据都会被丢弃,所有已写入的缓冲数据都不会被刷新到磁盘。
_exit函数通常在以下情况下使用:
- 进程需要立即退出,而不需要执行一些清理操作;
- 进程需要在调用fork函数之后要立即退出,以避免子进程继承父进程的某些状态。
需要注意的是,_exit函数不是标准C库函数,而是一个系统调用,因此在使用时需要包含
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行
echo $?
发现返回值是255。
exit函数是C语言标准库中的一个函数,用于终止进程的运行。其原型如下:
void exit(int status);
其中,status参数表示进程的退出状态码,通常情况下,0表示进程正常退出,非0表示进程异常退出。在调用exit函数之后,进程的所有打开文件都会被关闭,所有未写入的缓冲数据都会被写入文件,所有已写入的缓冲数据都会被刷新到磁盘。
需要注意的是,exit函数会先调用一些清理函数,例如关闭文件、清理缓存等,然后再调用_exit函数。因此,exit函数比直接调用_exit函数慢一些。
exit函数通常在以下情况下使用:
- 进程需要正常退出;
- 进程需要在执行一些清理操作之后退出。
exit函数是标准C库函数,因此在使用时需要包含
53 #include <stdio.h>
54 #include <unistd.h>
55 #include <stdlib.h>
68 int main()
69 {
70 pid_t ret = fork();
71 if(ret == 0)
72 {
73 printf("测试_exit函数");
74 _exit(-1);
75 }
76 sleep(5);
77 printf("测试exit函数");
78 exit(-1);
79 return 0;
80 }
在代码中我们分别测试了函数exit
和函数_exit
,使用子进程测试函数_exit
,停留5秒观察结果,然后运行父进程测试exit函数最后观察结果。
注意:printf函数中没有
\n
。
如下图:
由结果可以看到,_exit函数结束程序没有刷新缓冲区,而exit函数结束程序时会刷新缓冲区。
return退出是一种常见的退出进程方法。执行return等同于执行exit(n),返回值会被当做exit函数的参数传递给操作系统。因此,使用return语句和使用exit函数都可以用于退出程序,并且它们的效果是相同的。
需要注意的是,如果在main函数中使用return语句返回,程序会先执行一些清理操作,例如关闭文件、清理缓存等,然后再退出进程。而如果使用exit函数退出程序,清理操作和退出操作是一起执行的。
因此,如果不需要执行额外的清理操作,可以直接使用exit函数退出程序。如果需要在退出程序之前执行一些额外的清理操作,应该使用return语句返回。
进程等待非常重要,它可以避免出现僵尸进程和资源泄漏等问题。当一个子进程退出时,它的资源并不会立即被操作系统回收,而是会被保留在系统中,直到父进程通过进程等待的方式回收资源。如果父进程不进行进程等待,那么子进程就会变成僵尸进程,占用系统资源,甚至可能导致系统崩溃。因此,在编写程序时,我们需要注意在父进程中使用wait或waitpid函数对子进程进行等待。
系统给我们提供的有两个进程等待的函数,分别是wait
和waitpid
。
wait函数的原型为:
pid_t wait(int *status);
wait函数会阻塞父进程,直到任意一个子进程退出。如果父进程有多个子进程,那么wait函数会等待任意一个子进程退出,并返回该子进程的进程ID。wait函数可以获取子进程退出时的状态信息,该信息保存在参数status中。
waitpid函数的原型为:
pid_t waitpid(pid_t pid, int *status, int options);
waitpid函数可以指定等待的子进程,pid参数指定要等待的子进程ID,如果pid为-1,则表示等待任意一个子进程。waitpid函数还可以设置一些选项,例如非阻塞等待、只等待指定状态的进程等。waitpid函数也可以获取子进程退出时的状态信息,该信息保存在参数status中。
- 需要注意的是,wait和waitpid函数都可以阻塞父进程,直到子进程退出或出现错误。在使用这两个函数时,应该考虑防止死锁和优化程序性能等问题。
- wait函数和waitpid函数的使用都需要包含头文件
sys/types.h
和sys/wait.h
。
另外如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。 如果不存在该子进程,则立即出错返回。
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。 如果传递NULL,表示不关心子进程的退出状态信息。 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。 status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
进程的阻塞等待方式:
阻塞等待就是父进程一直停留再原地等待子进程运行结束后再运行,如下代码:
53 #include <stdio.h>
54 #include <unistd.h>
55 #include <stdlib.h>
56 #include <sys/wait.h>
57 #include <sys/types.h>
58 int main()
59 {
60 pid_t ret = fork();
61 if(ret < 0)
62 perror("fork");
63 else if (ret == 0)
64 {
65 for(int i = 0; i < 5; i++)
66 {
67 printf("子进程正在运行,pid : %d\n", getpid());
68 sleep(1);
69 }
70 }
71 else{
72 int status;
73 pid_t val = waitpid(-1, &status, 0);
74 printf("子进程运行结束\n");
75 if(WIFEXITED(status) && val == ret)
76 {
77 printf("正常结束,code: %d\n", WEXITSTATUS(status));
78 }
79 else{
80 printf("wait failed\n");
81 }
82 }
83 return 0;
84 }
运行结果:
从结果可以看出来,父进程再等待子进程运行的时候,父进程是一直停留在waitpid,当子进程结束后才运行接下来的代码。
进程的非阻塞等待方式:
非阻塞等待时,父进程会间接的查看子进程的运行状态,如果发现子进程没有运行结束那么他会做一些自己的事情,不会一直停在原地等待,如下代码:
53 #include <stdio.h>
54 #include <unistd.h>
55 #include <stdlib.h>
56 #include <sys/wait.h>
57 #include <sys/types.h>
58 int main()
59 {
60 pid_t ret = fork();
61 if(ret < 0)
62 perror("fork");
63 else if (ret == 0)
64 {
65 for(int i = 0; i < 5; i++)
66 {
67 printf("子进程正在运行,pid : %d\n", getpid());
68 sleep(1);
69 }
70 }
71 else{
72 int status;
73 pid_t val;
74 do{
75 val = waitpid(-1, &status, WNOHANG);
76 if(val == 0) printf("子进程再运行\n");
77 sleep(1);
78 }while(val == 0);
79 if(WIFEXITED(status) && val == ret)
80 {
81 printf("正常结束,code: %d\n", WEXITSTATUS(status));
82 }
83 else{
84 printf("wait failed\n");
85 }
86 }
87 return 0;
88 }
运行结果:
从结果可以看到,父进程在等待时,当子进程还没有运行结束,父进程会执行他接下来的代码。
进程程序替换是指一个进程调用另一个程序来替换自己的程序和数据,从而实现进程间的无缝衔接。在 Linux 系统中,替换函数是一组系统调用函数,用于将当前进程的执行上下文替换为另一个程序的执行上下文。常用的替换函数包括:
替换函数:有六种以exec开头的函数,统称为exec函数,如下图
函数 | 说明 |
---|---|
int execl(const char *path, const char *arg, …) | 使用参数列表传递命令行参数,第一个参数是要执行的程序的路径,第二个参数是程序的名称,后面的参数是程序的命令行参数。 |
int execlp(const char *file, const char *arg, …) | 和 execl() 函数类似,但是可以在 PATH 环境变量中查找要执行的程序。 |
int execle(const char *path, const char *arg, …, char * const envp[]) | 和 execl() 函数类似,但是可以通过环境变量传递参数。 |
int execv(const char *path, char *const argv[]) | 使用参数数组传递命令行参数,第一个参数是要执行的程序的路径,第二个参数是程序的命令行参数数组。 |
int execvp(const char *file, char *const argv[]) | 和 execv() 函数类似,但是可以在 PATH 环境变量中查找要执行的程序。 |
int execve(const char *filename, char *const argv[], char *const envp[]) | 和 execv() 函数类似,但是可以通过环境变量传递参数。 |
这些函数原型看起来很容易混,其实他们都是有规律的,
其中:
l(list) : 表示参数采用列表(以NULL结尾)
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。 如果调用出错则返回-1,因此exec函数只有出错的返回值而成功没有返回值。
测试代码:
1 #include <stdio.h>
2 #include <assert.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5 #include <sys/types.h>
6 int main()
7 {
8 printf("begin...........\n");
9 execl("/bin/ls", "ls", "-a", "-l", NULL);
10 printf("end...............\n");
11 return 0;
12 }
运行结果:
可以看到在进行程序替换后end就不打印了。
但是如果我们是在子进程里面进行替换是不影响父进程的,因为进程具有独立性。
事实上,如上的几种exec函数都是调用了execve函数,execve函数才是真正的系统调用函数,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在 man手册第3节。
在 Linux 中,进程控制是非常重要的一部分,除了进程创建和程序替换,进程控制还包括进程间通信。Linux 提供了多种进程间通信方式,如管道、消息队列、共享内存、信号量等。这些方式都是通过内核提供的机制来实现进程间数据传输和同步的。
总之,进程控制是 Linux 中非常重要的一部分,掌握进程控制的基本原理和常用函数,对于开发 Linux 应用程序和系统管理都非常有帮助。