Linux 进程终止 程序内调用其他可执行程序 创建进程 僵尸进程 多进程与信号(c++)

一.进程终止
        有 8 种方式可以中止进程,其中 5 种为正常终止,它们是:
1)在 main()函数用 return 返回;
2)在任意函数中调用 exit()函数;
3)在任意函数中调用_exit()或_Exit()函数;
4)最后一个线程从其启动例程(线程主函数)用 return 返回;
5)在最后一个线程中调用 pthread_exit()返回;
异常终止有 3 种方式,它们是:

6)调用 abort()函数中止;//不常用
7)接收到一个信号;
8)最后一个线程对取消请求做出响应。

1.进程终止的状态

        在main()函数中,return 的返回值即终止状态,如果没有 return 语句或调用 exit(),那么该进程的
终止状态是 0

在 Shell 中,查看进程终止的状态:echo $?
正常终止进程的 3 个函数(exit()和_Exit()是由 ISO C 说明的,_exit()是由 POSIX 说明的)。
void exit(int status);
void _exit(int status);
void _Exit(int status);

status 也是进程终止的状态。
如果进程被异常终止,终止状态为非 0。 服务程序的调度、日志和监控.

2.资源释放问题

        retun 表示函数返回,会调用局部对象的析构函数,main()函数中的 return 还会调用全局对象的析构函数。
exit()表示终止进程,不会调用局部对象的析构函数,只调用全局对象的析构函数。
exit()会执行清理工作,然后退出,_exit()和_Exit()直接退出,不会执行任何清理工作。

3.进程的终止函数
        进程可以用 atexit()函数登记终止函数(最多 32 个),这些函数将由 exit()自动调用。
int atexit(void (*function)(void));
exit()调用终止函数的顺序与登记时相反。 进程退出前的收尾工作。

二.调用可执行函数
        Linux 提供了 system()函数和 exec 函数族,在 C++程序中,可以执行其它的程序(二进制文件、操作系统命令或 Shell 脚本)

1.system()函数
        system()函数提供了一种简单的执行程序的方法,把需要执行的程序和参数用一个字符串传给
system()函数就行了。
函数的声明:
int system(const char * string);
system()函数的返回值比较麻烦。
1)如果执行的程序不存在,system()函数返回非 0;
2)如果执行程序成功,并且被执行的程序终止状态是 0,system()函数返回 0;
3)如果执行程序成功,并且被执行的程序终止状态不是 0,system()函数返回非 0。

2.exec 函数族
        exec 函数族提供了另一种在进程中调用程序(二进制文件或 Shell 脚本)的方法。
exec 函数族的声明如下:
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,...,char * const envp[]);

int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

注意:

1)如果执行程序失败则直接返回-1,失败原因存于 errno 中。
2)新进程的进程编号与原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈。
3)如果执行成功则函数不会返回,当在主程序中成功调用 exec 后,被调用的程序将取代调用者程序,也就是说,exec 函数之后的代码都不会被执行。
4)最常用的是 execl()和 execv(),其它的极少使用。

#include <iostream>
#include <string.h>
#include <unistd.h>
using namespace std;
int main(int argc,char *argv[])
{
int ret=execl("/bin/ls","/bin/ls","-lt","/tmp",0); // 最后一个参数 0 不能省略。
cout << "ret=" << ret << endl;
perror("execl");
/*
char *args[10];
args[0]="/bin/ls";
args[1]="-lt";
args[2]="/tmp";
args[3]=0; // 这行代码不能省略。
int ret=execv("/bin/ls",args);
cout << "ret=" << ret << endl;
perror("execv");
*/

两者区别就是execv()用字符数组把命令存储起来使用。

三.创建进程

1.Linux的0、1、2号进程
        整个 linux 系统全部的进程是一个树形结构。
0 号进程(系统进程)是所有进程的起源,它创建了 1 号和 2 号进程。
1 号进程(systemd)负责执行内核的初始化工作和进行系统配置。
2 号进程(kthreadd)负责所有内核线程的调度和管理。
pstree 命令可以查看进程树(yum -y install psmisc)。
pstree -p 进程编号

2.进程标识
每个进程都有一个非负整数表示的唯一的进程 ID。虽然是唯一的,但是进程 ID 可以复用。当一个
进程终止后,其进程 ID 就成了复用的候选者。Linux 采用延迟复用算法,让新建进程的 ID 不同于最近终止的进程所使用的 ID。这样防止了新进程被误认为是使用了同一 ID 的某个已终止的进程。
pid_t getpid(void); // 获取当前进程的 ID。
pid_t getppid(void); // 获取父进程的 ID。

3.fork()函数

        一个现有的进程可以调用 fork()函数创建一个新的进程。
pid_t fork(void);
由 fork()创建的新进程被称为子进程。子进程是父进程的副本,父进程和子进程都从调用 fork()之后的代码开始执行。

fork()函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是 0,而父进程的返回值则是子进程的进程 ID。
子进程获得了父进程数据空间、堆和栈的副本(注意:子进程拥有的是副本,不是和父进程共
享)。

fork()之后,父进程和子进程的执行顺序是不确定的。

#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
int num=7;
string message="test1...";
pid_t pid=fork();
if (pid>0)//返回值是子进程的进程ID的话大于0,所以执行父进程
{ // 父进程将执行这段代码。
sleep(1);
cout << "父:pid=" << pid << endl;
cout << "父:num:" << num <<" message:" << message << endl;
}
else
{ // 子进程将执行这段代码。
num=9; message="test2...";
cout << "子:pid=" << pid << endl;
cout << "子:num:" <<num<<"  message" << message << endl;
}

4.fork()的两种用法
1)父进程复制自己,然后,父进程和子进程分别执行不同的代码。这种用法在网络服务程序中很常见,父进程等待客户端的连接请求,当请求到达时,父进程调用 fork(),让子进程处理些请求,而父进程则继续等待下一个连接请求。
2)进程要执行另一个程序。这种用法在 Shell 中很常见,子进程从 fork()返回后立即调用 exec。

#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
if (fork()>0)
{ // 父进程将执行这段代码。
while (true)
{
sleep(1);
cout << "父进程运行中...\n";
}
}
else
{ // 子进程将执行这段代码。
sleep(10);
cout << "子进程开始执行任务...\n";
execl("/bin/ls","/bin/ls","-lt","/tmp",0);
cout << "子进程执行任务结束,退出。\n";
}
}

5.共享文件

fork()的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程和子进程共享同一
个文件偏移量。
如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么它们的输出可能会
相互混合。

#include <iostream>
#include <fstream>
#include <unistd.h>
using namespace std;
int main()
{
ofstream fout;
fout.open("/tmp/tmp.txt"); // 打开文件。
fork();
for (int ii=0;ii<10000000;ii++) // 向文件中写入一千万行数据。
{
fout << "进程" << getpid() << "安宥真" << ii <<“最漂亮" << "\n"; //纯属个人喜好
}
fout.close(); // 关闭文件。
}

6.vfork()函数
vfork()函数的调用和返回值与 fork()相同,但两者的语义不同。
vfork()函数用于创建一个新进程,而该新进程的目的是 exec 一个新程序,它不复制父进程的地址
空间,因为子进程会立即调用 exec,于是也就不会使用父进程的地址空间。如果子进程使用了父进程的地址空间,可能会带来未知的结果。

vfork()和 fork()的另一个区别是:vfork()保证子进程先运行,在子进程调用 exec 或 exit()之后父进
程才恢复运行。

四。僵尸进程

        如果父进程比子进程先退出,子进程将被 1 号进程托管(这也是一种让程序在后台运行的方法)。
如果子进程比父进程先退出,而父进程没有处理子进程退出的信息,那么,子进程将成为僵尸进程。
僵尸进程有什么危害?内核为每个子进程保留了一个数据结构,包括进程编号、终止状态、使用 CPU 时间等。父进程如果处理了子进程退出的信息,内核就会释放这个数据结构,父进程如果没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程的进程编号将一直被占用。系统可用的进程编号是有限的,如果产生了大量的僵尸进程,将因为没有可用的进程编号而导致系统不能产生新的进程。


僵尸进程的避免:
1)子进程退出的时候,内核会向父进程发头 SIGCHLD 信号,如果父进程用 signal(SIGCHLD,SIG_IGN)通知内核,表示自己对子进程的退出不感兴趣,那么子进程退出后会立即释放数据结构。
2)父进程通过 wait()/waitpid()等函数等待子进程结束,在子进程退出之前,父进程将被阻塞待。
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);

返回值是子进程的编号。
stat_loc 是子进程终止的信息:a)如果是正常终止,宏 WIFEXITED(stat_loc)返回真,宏WEXITSTATUS(stat_loc)可获取终止状态;b)如果是异常终止,宏 WTERMSIG(stat_loc)可获取终止进程的信号。

3)如果父进程很忙,可以捕获 SIGCHLD 信号,在信号处理函数中调用 wait()/waitpid()。

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
if (fork()>0)
{ // 父进程的流程。
int sts;
pid_t pid=wait(&sts);
cout << "已终止的子进程编号是:" << pid << endl;
if (WIFEXITED(sts)) { cout << "子进程是正常退出的,退出状态是:" << WEXITSTATUS(sts)
<< endl; }
else { cout << "子进程是异常退出的,终止它的信号是:" << WTERMSIG(sts) << endl; }
}
else
{ // 子进程的流程。
//sleep(100);
int *p=0; *p=10;
exit(1);
}
}
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void func(int sig) // 子进程退出的信号处理函数。
{
int sts;
pid_t pid=wait(&sts);
cout << "已终止的子进程编号是:" << pid << endl;
if (WIFEXITED(sts)) { cout << "子进程是正常退出的,退出状态是:" << WEXITSTATUS(sts) <<
endl; }
else { cout << "子进程是异常退出的,终止它的信号是:" << WTERMSIG(sts) << endl; }
}
int main()
{
signal(SIGCHLD,func); // 捕获子进程退出的信号。
if (fork()>0)
{ // 父进程的流程。
while (true)
{
cout << "父进程忙着执行任务。\n";
sleep(1);
}
}
else
{ // 子进程的流程。
sleep(5);
// int *p=0; *p=10;
exit(1);
}
}

五.多进程与信号
        在多进程的服务程序中,如果子进程收到退出信号,子进程自行退出,如果父进程收到退出信号,则应该先向全部的子进程发送退出信号,然后自己再退出。

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void FathEXIT(int sig); // 父进程的信号处理函数。
void ChldEXIT(int sig); // 子进程的信号处理函数。
int main()
{
// 忽略全部的信号,不希望被打扰。
for (int ii=1;ii<=64;ii++) signal(ii,SIG_IGN);
// 设置信号,在 shell 状态下可用 "kill 进程号" 或 "Ctrl+c" 正常终止些进程
// 但请不要用 "kill -9 +进程号" 强行终止
signal(SIGTERM,FathEXIT); signal(SIGINT,FathEXIT); // SIGTERM 15 SIGINT 2
while (true)
{
if (fork()>0) // 父进程的流程。
{
sleep(5); continue;
}
else // 子进程的流程。
{
// 子进程需要重新设置信号。
signal(SIGTERM,ChldEXIT); // 子进程的退出函数与父进程不一样。
signal(SIGINT ,SIG_IGN); // 子进程不需要捕获 SIGINT 信号。
while (true)
{
cout << "子进程" << getpid() << "正在运行中。\n"; sleep(3); continue;
}
}
}
}
// 父进程的信号处理函数。
void FathEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "父进程退出,sig=" << sig << endl;
kill(0,SIGTERM); // 向全部的子进程发送 15 的信号,通知它们退出。
// 在这里增加释放资源的代码(全局的资源)。
exit(0);
}
// 子进程的信号处理函数。
void ChldEXIT(int sig)
{
// 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);
cout << "子进程" << getpid() << "退出,sig=" << sig << endl;
// 在这里增加释放资源的代码(只释放子进程的资源)。
exit(0);

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值