Linux进程编程

2022-06-17 14:34:24 浏览数 (1)

Linux进程编程
  • 3.1 fork系统调用
    • 3.1.1 fork工作原理
    • 3.1.2 fork函数
    • 3.1.3 fork编程示例
    • 3.1.4 小结
  • 3.2 exec系统调用
    • 3.2.1 exec函数族作用
    • 3.2.2 exec函数族
    • 3.2.3 exec编程示例
    • 3.2.4 小结
  • 3.3 exit系统调用
    • 3.3.1 exit工作原理
    • 3.3.2 exit函数
  • 3.4 wait系统调用
    • 3.4.1 wait工作原理
    • 3.4.2 wait函数
    • 3.4.3 进程管理综合示例

3.1 fork系统调用

3.1.1 fork工作原理

fork系统调用是创建子进程的唯一方法。执行过程如下:

  1. Linux内核在进程表中为子进程分配一个表项,然后分配PID。子进程表项的内容来自父进程,fork会将父进程的表项复制为副本,并分配给子进程;
  2. Linux内核使父进程的文件表和索引表的节点自增1,创建用户及上下文;
  3. 将父进程上下文复制到子进程上下文空间中;
  4. fork调用结束后子进程的PID将返回给父进程,而子进程获得的值为0。

3.1.2 fork函数

头文件:unistd.h

函数原型:pid_fork(void)

功能:创建一个与原来进程几乎完全相同的进程,即两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork函数后,系统先给新的进程分配资源,例如,存储数据和代码的空间。然后把原来的进程所有值都复制到新的进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

返回值:fork被调用一次却能够返回两次且可能有三种不同的返回值:

  1. 在父进程中,fork返回新创建子进程的进程ID(通常为父进程PID 1);
  2. 在子进程中,fork返回0;
  3. 如果出现错误,fork返回一个负值。 fork出错可能有两种原因:
  4. 当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN;
  5. 系统内存不足,这时errno的值被设置为ENOMEM。 两个进程有不同的PID,但执行顺序由进程调度策略决定。

3.1.3 fork编程示例

  1. 打开Ubuntu终端,切换用户到root,新建一个process文件夹用于存放实验文件,进入该目录下,输入sudo vi forkProcess.c使用vi文本编辑器编辑forkProcess.c文件;
  1. 按下i键进入编辑模式,输入fork编程示例,该示例创建一个子进程,通过fork()函数返回值判断进程是子进程还是父进程,并打印信息。
  1. 按下ESC键退出编辑模式,输入“:wq”回到命令行,使用gcc编译器编译forkProcess.c文件生成可执行文件forkProcess.
  1. 输入./forkProcess执行该文件,得到结果如下:

3.1.4 小结

其实,fork底层是调用了内核的函数来实现fork的功能的,即先create()先创建进程,此时进程内容为空,然后clone()复制父进程的内容到子进程中,此时子进程就诞生了,接着父进程就return返回了。而子进程诞生后,是直接运行return返回的,然后接着执行后面的程序,这里注意:子进程是不会执行前面父进程已经执行过的程序了得,因为PCB中记录了当前进程运行到哪里,而子进程又是完全拷贝过来的,所以PCB的程序计数器也是和父进程相同的,所以是从fork()后面的程序继续执行。此时就按照前面的规则进行判断返回[1]。如下图所示:

3.2 exec系统调用

3.2.1 exec函数族作用

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。这里的可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

与一般情况不同,exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

我们应该明白了,Linux下是如何执行新程序的,每当有进程认为自己不能为系统和用户做出任何贡献了,他就可以发挥最后一点余热,调用任何一个exec,让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork出一个新进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生了一个新进程一样[2]。

3.2.2 exec函数族

头文件:unistd.h

函数原型:

int execl(const char *path, const char *arg, …);

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

int execle(const char *path, const char *arg,…, char * const envp[]);

int execve(const char *file, char *const argv[]);

int execlp(const char *file, const char *arg, …);

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

选项:

​ l:希望接收一个以逗号分隔的参数列表,列表以NULL指针作为结束标志;

​ v:希望接收一个以NULL结尾的字符串数组的指针;

​ p:是一个以NULL结尾的字符串数组指针,函数可以利用PATH变量查找子程序文件;

​ e:函数传递指定参数envp,允许改变子进程的环境,无后缀e时子进程使用当前程序的环境。

参数:

​ path:可执行文件的路径名字;

​ arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束;

​ file:如果参数file中包含/,就将其视为路径名,否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。

功能:

以新进程代替原有进程,但PID保持不变。

返回值:

执行成功不返回,出错则返回-1,失败原因记录在errno中。

六个函数的区别:

  1. 查找方式不同:前四个函数的查找方式都是完整的文件目录路径,而后两个(以p结尾的两个函数)可以只给出文件名,系统会自动从环境变量“$PATH”所指的路径中进行查找;
  2. 参数传递方式不同:exec函数族的参数传递有两种方式:函数名的第五位字母为“l”(list)表示逐个列举的方式、函数名第五位字母为“v”(vector)的表示将所有参数整体构造成指针数组传递。然后将该数组的首地址当作参数传给它,数组中的最后一个指针要求是NULL;
  3. 环境变量不同:以“e”(environment)结尾的两个函数execl、execve可以在envp[]中指定当前进程所使用的的环境变量替换掉该进程继承的环境变量,其他函数把调用进程的环境传递给新进程。

3.2.3 exec编程示例

execl 实现ls指令

execv 实现获取系统时间

3.2.4 小结

执行exec系统调用,一般都是这样,用fork()函数新建立一个进程,然后让进程去执行exec调用。我们知道,在fork()建立新进程之后,父进程与子进程共享代码段,但数据空间是分开的,但父进程会把自己数据空间的内容copy到子进程中去,还有上下文也会copy到子进程中去。而为了提高效率,采用一种写时copy的策略,即创建子进程的时候,并不copy父进程的地址空间,父子进程拥有共同的地址空间,只有当子进程需要写入数据时(如向缓冲区写入数据),这时候会复制地址空间,复制缓冲区到子进程中去。从而父子进程拥有独立的地址空间。而对于fork()之后执行exec后,这种策略能够很好的提高效率,如果一开始就copy,那么exec之后,子进程的数据会被放弃,被新的进程所代替。

3.3 exit系统调用

3.3.1 exit工作原理

exit系统调用的执行发生以下事件:

  1. 清除当前进程的所有信号处理函数。
  2. 如果当前进程是和终端关联的“进程组组长”,则会向每个组内进程发送hang-up signal,并且把这些成员的进程组设置为0。
  3. 通过内核内部算法关闭当前进程所有打开的文件描述符,并且释放当前目录所关联的inode;如果存在current (charged)root,也将其释放通过算法iput。
  4. 为进程释放所有的region以及关联的memory。
  5. 计算进程机器子进程执行的时间(user mode 和kernel mode),并把记录写到一个全局的accounting file。
  6. 将进程的状态改变为zombie,并将自己的所有的子进程的父进程ID设置为1(init);如果有孩子的状态是zombie,向init进程发SIGCHLD信号,以清除子进程的process table slot。
  7. exiting进程向自己的父进程发送SIGCHLD信号。
  8. 进行context switch,调度其他非zombie进程(本进程已经是zombie)。

3.3.2 exit函数

函数原型1:void _exit(int status)

头文件:unistd.h

函数原型2:void exit(int status)

头文件:stdlib.h

功能:终止发出调用的进程,status是返回给父进程的状态值,父进程可通过wait系统调用获得。

exit()和_exit()的区别:

l _exit()的作用最简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;

l exit()在终止进程之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,即清理“I/O缓冲”;

l 两者最终都要将控制权交给内核,旖旎次,要想保证数据的完整性,就一定要使用exit()。

3.4 wait系统调用

3.4.1 wait工作原理

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

3.4.2 wait函数

头文件:sys/types.h和sys/wait.h

函数原型1:pid_t wait(int *status)

函数原型2:pid_t waitpid(pid_t pid, int *status, int options)

参数:

status:返回子进程退出时的状态;

pid:pid>0时:等待进程号为pid的子进程结束、pid=0时:等待组ID等于调用进程组ID的子进程结束、pid=-1时:等待任一子进程结束,等价于调用wait()、pid<-1时:等待组ID等于PID的绝对值的任一子进程结束;

options:WNOHANG:若pid指定的子进程没有结束,则waitpid()不阻塞而立即返回,此时的返回值为0、WUNTRACED:为了实现某种操作,由pid指定的任一进程已被暂停,且其状态自暂停以来还未报告过,则返回其状态、0:同wait(),阻塞父进程,等待子进程退出。

wait()与waitpid()区别:wait()函数等待所有子进程的僵死状态,waitpid()函数等待PID与参与pid相关的子进程的僵死状态。

检查子进程的返回状态码status:

WIFEXITED(status):进程中通过调用_exit()或exit()正常退出,该宏值为非0;

WIFSIGNALED(status):子进程因得到的信号没有被捕捉导致退出,该宏值为非0;

WIFSTOPPED(status):子进程没有终止但停止了,并可重新执行时,该宏值为非0,这种情况仅出现在waitpid()调用中使用了WUNTRACED选项;

WEXITSTATUS(status):如果WIFEXITED(status)返回非0,该宏返回由子进程调用_exit(status)或exit(status)时设置的调用参数status值;

WTERMSIG(status):如果WIFSIGNALED(status)返回非0,该宏返回导致子进程退出的信号的值;

WSTOPSIG(status):如果WIFSTOPPED(status)返回非0,该宏返回导致子进程停止的信号的值。

3.4.3 进程管理综合示例

0 人点赞