【Linux系统】详解,进程控制

发布于:2025-08-03 ⋅ 阅读:(17) ⋅ 点赞:(0)

前言:

        上文我们讲到了Linux中的虚拟空间地址,知道了一个进程对应一个虚拟地址空间,虚拟空间地址与物理地址之间通过页表映射....【Linux】虚拟地址空间-CSDN博客

        本文我们来讲一讲Linux系统是如何控制进程的!

        如果喜欢本期文章,请点点关注吧!非常感谢佬的支持   _(:з」∠)_                                 


进程创建

fork函数

        fork函数是Linux系统提供的接口,其功能就是创建子进程。

        既调用fork函数,系统就自动为我们创建好了子进程

#include<unistd.h>
pid_t fork();

其中pid_t是Linux中的数据类型,相当于int,即为整型

        fork的返回值有两个,对于父进程:返回子进程的pid,对于子进程:返回0

#include <stdio.h>
#include <unistd.h>
 
int main()
{
    pid_t pid = fork();
    if(pid<0)
    {   
        printf("创建失败");
    }   
    else if(pid == 0)
    {   
        //子进程
        printf("我是一个子进程:%d,这是我的父进程:%d\n",getpid(),getppid());
    }   
    else if(pid > 0)
    {   
       printf("我是一个父进程:%d,这是我的父进程:%d\n",getpid(),getppid());
    }   
} 

        对于fork原理的详细介绍可参考【Linux】初见,进程概念-CSDN博客中的第四节“如何创建进程”,这里面有超级详细的介绍!

fork的用法

        一个父进程希望复制自己,使父子进程同时执行不同的代码段。e.g. 父进程等待客户端响应,生成子进程处理请求。

        一个进程想要执行多个不同的代码。e.g. 生成子进程调用exec函数。

fork失败原因

        系统中的进程太多了

        用户的进程数量超过了限制


进程终止

进程终止的本质就是进程结束,系统释放资源:释放进程申请的相关数据结构和对应的代码与数据

进程退出的场景

        1.代码运行完毕,结果正确(退出码为0)

        2.代码运行完毕,结果不正确(退出码为非0)

        3.代码异常终止(进程接收到型号终止,退出码无意义)

进程退出方法

正常退出:

        1.从main函数返回

        2.调用exit

        3._exit

异常退出:

        信号终止

退出码

        退出码可以告诉我们进程终止时的状态,0代表执行成功,非0则代表不成功

        非0的退出码中,一个值对应一个错误原因。可以使用strerror函数获取退出码对应的信息。

#include <stdio.h>
#include<string.h>

int main()
{
    for(int i=0;i<200;i++)
    {   
        printf("%d -> %s\n",i,strerror(i));                                                                                                                            
    }   
}
0 -> Success
1 -> Operation not permitted
2 -> No such file or directory
3 -> No such process
4 -> Interrupted system call
5 -> Input/output error
6 -> No such device or address
7 -> Argument list too long
8 -> Exec format error
9 -> Bad file descriptor
10 -> No child processes
11 -> Resource temporarily unavailable
12 -> Cannot allocate memory
13 -> Permission denied
14 -> Bad address
15 -> Block device required
16 -> Device or resource busy
17 -> File exists
18 -> Invalid cross-device link
19 -> No such device
20 -> Not a directory
21 -> Is a directory
22 -> Invalid argument
23 -> Too many open files in system
24 -> Too many open files
25 -> Inappropriate ioctl for device
26 -> Text file busy
27 -> File too large
28 -> No space left on device
29 -> Illegal seek
30 -> Read-only file system
31 -> Too many links
32 -> Broken pipe
33 -> Numerical argument out of domain
34 -> Numerical result out of range
35 -> Resource deadlock avoided
36 -> File name too long
37 -> No locks available
38 -> Function not implemented
39 -> Directory not empty
40 -> Too many levels of symbolic links
41 -> Unknown error 41
42 -> No message of desired type
43 -> Identifier removed
44 -> Channel number out of range
45 -> Level 2 not synchronized
46 -> Level 3 halted
47 -> Level 3 reset
48 -> Link number out of range
49 -> Protocol driver not attached
50 -> No CSI structure available
51 -> Level 2 halted
52 -> Invalid exchange
53 -> Invalid request descriptor
54 -> Exchange full
55 -> No anode
56 -> Invalid request code
57 -> Invalid slot
58 -> Unknown error 58
59 -> Bad font file format
60 -> Device not a stream
61 -> No data available
62 -> Timer expired
63 -> Out of streams resources
64 -> Machine is not on the network
65 -> Package not installed
66 -> Object is remote
67 -> Link has been severed
68 -> Advertise error
69 -> Srmount error
70 -> Communication error on send
71 -> Protocol error
72 -> Multihop attempted
73 -> RFS specific error
74 -> Bad message
75 -> Value too large for defined data type
76 -> Name not unique on network
77 -> File descriptor in bad state
78 -> Remote address changed
79 -> Can not access a needed shared library
80 -> Accessing a corrupted shared library
81 -> .lib section in a.out corrupted
82 -> Attempting to link in too many shared libraries
83 -> Cannot exec a shared library directly
84 -> Invalid or incomplete multibyte or wide character
85 -> Interrupted system call should be restarted
86 -> Streams pipe error
87 -> Too many users
88 -> Socket operation on non-socket
89 -> Destination address required
90 -> Message too long
91 -> Protocol wrong type for socket
92 -> Protocol not available
93 -> Protocol not supported
94 -> Socket type not supported
95 -> Operation not supported
96 -> Protocol family not supported
97 -> Address family not supported by protocol
98 -> Address already in use
99 -> Cannot assign requested address
100 -> Network is down
101 -> Network is unreachable
102 -> Network dropped connection on reset
103 -> Software caused connection abort
104 -> Connection reset by peer
105 -> No buffer space available
106 -> Transport endpoint is already connected
107 -> Transport endpoint is not connected
108 -> Cannot send after transport endpoint shutdown
109 -> Too many references: cannot splice
110 -> Connection timed out
111 -> Connection refused
112 -> Host is down
113 -> No route to host
114 -> Operation already in progress
115 -> Operation now in progress
116 -> Stale file handle
117 -> Structure needs cleaning
118 -> Not a XENIX named type file
119 -> No XENIX semaphores available
120 -> Is a named type file
121 -> Remote I/O error
122 -> Disk quota exceeded
123 -> No medium found
124 -> Wrong medium type
125 -> Operation canceled
126 -> Required key not available
127 -> Key has expired
128 -> Key has been revoked
129 -> Key was rejected by service
130 -> Owner died
131 -> State not recoverable
132 -> Operation not possible due to RF-kill
133 -> Memory page has hardware error

        使用echo $?,可以打印出最近一个程序的退出码。

        退出码是进程的性质之一,所以退出码会保存到进程的PCB中。

_exit函数

#include<unistd.h>
void _exit(int status);

在任何地方调用_exit函数,都会让当前进程结束
并以给定的值作为退出码退出

exit函数

#include<unistd.h>
void exit(int status);

与_exit函数功能类似
都是以指定的退出码,退出当前进程

区别

        _exit函数系统调用,而exit是C语言提供。

        使用exit函数退出时,会进行缓冲区的刷新。反之_exit则不会。

举个例子说明:

        程序结束刷新缓冲区,信息打印在屏幕上,反之没有信息打印

        感兴趣的朋友可以看看这篇文章,这里有详细的缓冲区刷新介绍【Linux】LInux下第一个程序:进度条-CSDN博客

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

int main()
{
    printf("yuzuriha");
    sleep(1);
    exit(0);                                                                                                                                                           
}

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

int main()
{
    printf("yuzuriha");                                                                                                                                                
    sleep(1);
    _exit(0);
}


进程等待

进程等待的必要性

        1.之前我们讲过子进程的退出后,子进程回进入僵尸状态,必须要被父进程回收才行。若子进程一直没有被父进程回收,就会一直处于僵尸状态,进而造成内存泄漏。

        2.进程被创造出来执行任务,结果如何,是成功、失败还是异常,这是父进程要得知的必要信息。

        所以回收子进程是必要的,父进程通过进程等待的方式,回收子进程进程资源、获取子进程的退出信息。

进程等待的方法

wait方法
#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status);

返回值:等待成功返回对应的子进程pid,失败则返回-1

参数status:为输出型参数,可以获取子进程的退出码
            若不需要,可以传NULL忽略这个参数
waitpid函数
#include<sys/types.h>
#include<sys/wait.h>

pid_ t waitpid(pid_t pid, int *status, int options);

下面详细介绍一下每一个参数:

pid_t pid

参数pid:-1,表示可以等待任意一个子进程。

参数pid:>0,表示只能等待当前这个进程的子进程。

status

        如上面所讲,status是一个输出型参数,由操作系统填充,我们可以通过status获取进程状态

        status并不是一个简单的整型,细节如下:

        1.status是一个低16位有效的int,通过位划分存储子进程的信息

        2.正常终止的情况下:低7位全为0,15~8存放退出码

                获取退出码:WEXITSTATUS(status)

        3.非正常退出:退出码无意义,第7位存放标志,6~0存放信号编号

                判断一个进程是否位正常退出:WIFEXITED(status),正常为真。

options

        options的默认值是0,表示阻塞等待

        options为:WNOHANG时表示非阻塞等待

        简而言之,阻塞等待就是等待子进程结束的过程中,父进程不能运行。而非阻塞等待,就是等待过程中父进程可以执行自己的代码。

        当参数为:WNOHANG时,waitpid会进行非阻塞轮询等待结束,返回>0(pid)本次调用结束,但子进程还没有退出,返回0调用失败,返回<0;


进程程序替换 

        程序替换就是字面上的意思,替换掉原有的程序,先来直接看看效果

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

int main()
{
    printf("开始\n");
    execl("/usr/bin/ls","ls","-l",NULL);
    printf("结束\n");
}

         我们可以看到执行就第一个printf打印出了结果,第二行就执行就程序替换,将原来的程序替换为了ls指令。

替换原理

        exec系列函数,其功能就是用新的程序替换原本的程序。

        如图,替换程序的本质是将物理内存中的数据,用新数据覆盖,页表中的物理地址重新覆盖填写,其他的不动。

注意:

        1.一旦程序替换成功,就会去执行新代码了,旧代码以及不复存在了。

        2.exec系列函数只有在失败的时候才有返回值,既只要返回就是失败。

        3.程序替换并没有创建新进程,只是进行了数据的覆盖。

认识全部exec函数

execl
int execl(const char* path , const char* arg , ...)

l:代表list,以链表的形式传参
path:路径+程序名,表示要执行谁
arg:平时怎么写指令,这里就怎么写。表示如何执行

注:一定要以NULL结尾!!!
#include <stdio.h>                                                                                                                                                     
#include<unistd.h>

int main()
{
    printf("开始\n");
    execl("/usr/bin/ls","ls","-l",NULL);
    printf("结束\n");
}

不仅可以替换系统程序,也可以替换我们自己写的程序:

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

int main()
{
    printf("开始\n");
    execl("./x","./x",NULL);                                                                                                                                           
    printf("结束\n");
}

        我们知道数据和代码父子进程默认是共享的,那程序替换对父进程有影响吗?

        没有,因为数据和代码都会进行写时拷贝

execlp
int execlp(const char* file , const char* arg , ...)

l:表示list,以链表的形式传参
p:表示环境变量PATH

file:有了PATH环境变量,我们就不用写路径了。
      直接写我们要执行的程序名即可
arg:同上,与我们平时写的指令无异
     但必须以NULL结尾
#include <stdio.h>
#include<unistd.h>

int main()
{
    printf("开始\n");
    execlp("ls","ls","-l",NULL);                                                                                                                                       
    printf("结束\n");
}

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

v:表示vector,用数组的方式传参
   数组中,依旧想要以NULL结尾
#include <stdio.h>
#include<unistd.h>

int main()
{
    char* const argv[]={
        (char* const)"ls",
        (char* const)"-l",
        NULL
    };  

    printf("开始\n");
    execv("/usr/bin/ls",argv);                                                                                                                                         
    printf("结束\n");
}

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

v:vector,以数组形式传参
p:环境变量PATH

同上,就不举例了

execvpe
int execvpe(const char* flie , char* const argv[] , char* cosnt envp[])

相比上面,多了一个e
e:环境变量!

传入envp数组,那被替换的进程的环境变量会被evnp[]直接覆盖
这会导致我们无法使用原来的环境变量

        所以我们一般采用新增环境变量的方式,而不是直接覆盖。

        法一:调用系统调用:putevn(char* string)新增环境变量,再调用其他exec函数实现

        法二:调用系统调用:putevn(char* string)新增环境变量,调用execvpe时,传入环境变量指针:environ

execle
int execle(const char* path , char* const argv .... , char* cosnt envp[])

同上
execve
int execve(const char* path , char* const argv[] ... , char* cosnt envp[])

同上

值得一提的是execve的系统调用,而其他函数都是C语言提供的


网站公告

今日签到

点亮在社区的每一天
去签到