Linux进程进阶

本文深入讲解进程创建、控制与终止的细节,包括system、fork、exec函数族的应用,孤儿与僵尸进程的处理,以及进程终止的各种方式。同时,探讨了守护进程的特性和编程规则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

缓冲区必须刷新才可以写到磁盘上
int fsync(int fd);//等价于fflush(),刷新同步
标准输出一定要加"\n"进行刷新
fflush(stdout);

一、进程的创建

1.system函数

<1>一个进程正常启动时间是几百毫秒,而用system("sleep"),要拉起sleep进程,慢

<2> system通过./a.out执行后,拉起来了sh -c进程和脚本进程两个进程

创建进程(几百毫秒)的耗时可以执行几百条指令

system使用了fork和exec两个函数

<3>system("ls -l"); //类似于嵌脚本

 

2.fork函数

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

 

fork()从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程。他与其他函数的区别在于:

它执行一次返回两个值,其中父进程的返回值是子进程的进程号,而子进程的返回值为零,若出错返回-1,可以通过返回值来判断父进程还是子进程。

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

fork创建子进程的过程:

使用fork函数得到的子进程,它从父进程继承了进程的地址空间,包括进程上下文,进程堆栈,内存信息,打开的文件描述符,信号控制设定,进程优先级,进程组号,当前工作目录,根目录,资源限制,控制终端,而子进程所独有的只有它的进程号,资源使用和计时器等。通过这种复制方式创建出子进程后,原有进程和子进程都从函数fork返回,各自继续往下运行,但是原进程的fork返回值与子进程的fork返回值不同,在原进程中,fork返回子进程的pid,而在子进程中fork返回零,如果fork返回负值,表示创建子进程失败。

 

fork函数会把原有PCB结构体复制一遍,改变为新的pid(pid + 1)

父进程先fork出来,在上一个调度周期就执行完,所以先打印,先返回;子进程的pid在就绪队列排队,后打印;

父子进程共享0,1,2管道

int main()
{
    pid_t pid;
    pid = fork();//fork之后马上跟if
    if (0 == pid) {
        printf("I am child, mypid = %d, myppid = %d\n", getpid(), getppid());
        return 0;//子
    } else {
        printf("I am parent, pid = %d\n", getpid());
        return 0;//父进程,进程组组长
    }//走到这里时,两个PCB都出来了,所以下面的语句父子进程会各执行一次
} 
//上述代码存在的问题是会造成父进程先打印完之后关闭了标准输入输出
//导致子进程无法打印完毕;
//解决办法是把父进程添加进sleep(1),父进程先睡一秒

子进程具有独立的进程号,资源使用和计时器

 

一个全局变量int i,不阻碍各自的资源使用,可以控制各自的虚拟内存和栈空间

lazy模式:写时复制,父子的虚拟内存中存的i都是10,指向同一个物理地址空间,当父进程单独改变i为5时,由于引用计数大于1,物理内存空间自动开辟一个新的空间并赋值为10,由子进程重新指向,产生新的映射,而原来的空间修改为父进程的i=5。

 

写时拷贝是在需要修改对象时才真正去开辟空间拷贝数据,如果只是读数据,则只需浅拷贝即可。写时拷贝是通过引用计数实现的,当有对象需要修改其指向空间的值时,则需要重新开辟空间,保证修改此对象的值不会影响其他对象的值,此时要将旧空间的引用计数减1,新空间的引用计数加1。

 

文件描述符的fork

假如要打印文件出来,文件是在共享内存里面的,父进程读完文件之后,指针指到文件末尾了,所以在子进程那里需要lseek到文件开头SEEK_SET,使用的是dup机制,引用计数等于2,父进程close(fd)时,引用计数变为1,子进程依然可以使用到fd,对于文件的打开,没有用到写时复制(文件锁操作磁盘效率低,现在用的是内存锁)文件打开针对fork是dup机制。(具体参考下篇博客)

 

3.exec函数族

<1>execl()

int execl(const char* path, const char* arg,...);
//path:包括执行文件名的全部路径名
//arg:可执行文件的命令行参数,多个用,分割注意最后一个参数为NULL
//execl有去无回,成功就没有返回值
add.c
int main(int argc, char* argv[])
{
    int i = atoi(argv[1]);
    int j = atoi(argv[2]);
    printf("sum = %d\n", i + j);
    return 0;
}

gcc add.c -o add

int main()
{
    execl("./add", "add", "3", "4", NULL);
    printf("you can't see me\n");
    return 0;
}//execl函数成功不返回,失败才返回-1

显示结果只有:sum=7

鸠占鹊巢同一个进程号,但是代码段被灌

当执行execl的时候,进程execl变成add,代码段马上被add的代码占据,代码段被灌(系统调用只有fork和exec*两种创建进程),很少用execl

所有新进程起来都需要:bash进程先fork一个子进程,子进程执行execl把需要启动的代码拉起来。(灌代码段,fork复制)

systm(“sleep 10”)的工作过程:
./a.out先fork一个sh -c sleep 10子进程出来,
sh -c sleep 10执行要执行的命令sleep(10)

<2>execv函数

 int main()
 {
     char* arr[2] = {argv[2], NULL};
     execv(argv[1], arr);
     printf("you can't see me\n");
     return 0;
 }

监视进程:每5秒监控业务进程是否挂掉

业务进程:监视进程拉起业务进程(./)

 

二、进程控制与终止

1.进程的控制

<1>孤儿进程

父进程先于子进程退出,子进程还没退出,则子进程成为孤儿进程;

此时自动被pid为1的进程接管,孤儿进程退出后,它的清理工作有祖先进程init自动处理,但在init进程清理子进程之前,它一直消耗系统的资源,所以要尽量避免。(清理工作:回收PCB结构体,free掉结构体空间)

int main()
{
    pid_t pid = fork();
    if (pid == 0) {
        while(1);//子
    } else {
        exit(10);//父
    }
}

<2>僵尸进程

如果子进程先退出,系统不会自动清理掉子进程的环境PCB,而必须由父进程调用wait或waitpid函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为 僵尸进程(defunct)zombie(子进程先完成使命,父进程需要做最后输入输出工作,所以还没退出)

int main()
{
    pid_t pid = fork();
    if (pid == 0) {
        exit();//子
    } else {
        while(1);//父
    }
}

通过ps -elf|grep a.out查看进程 状态为Z(下一步就是资源被回收,不能换成R,S,T)

 

wait()和waitpid()函数的使用:父进程等待子进程

父进程执行到wait()函数,进入sleep状态,等待子进程发出17号SIGCHLD信号,然后父进程执行清理工作

wait函数随机的等待一个已经退出的子进程,并返回一个已经退出的子进程,并进行清理工作

waitpid函数等待指定pid的子进程,如果为-1表示等待所有子进程

一个wait或waitpid一次只能回收一个进程

wait(NULL);//避免僵尸进程,成功返回pid,失败返回-1
int main()
{
    int i = 10;
    pid_t pid;
    pid = fork();
    if (0 == pid)
    {
        printf("I am child, mypid = %d, myppid = %d\n", getpid(), getppid());
        return 0;
    } else {
        printf("I am parent, pid = %d, mypid = %d\n", pid, getpid());
        wait(NULL);//NULL表示等待所有进程
        while(1);//把父进程设置为死循环,便于查看进程
        return 0;
    }
}
waitpid()//函数应用//指定回收对应进程,可设置是否等待
//父进程执行到wait函数时,等待子进程结束,再进行回收

int main()
{
    pid_t pid; 
    pid = fork();
    if (0 == pid) {
        printf("I am child, mypid = %d, myppid = %d\n", getpid(), getppid());
        sleep(3);
        //char* p = NULL;子进程崩溃
        //*p = 'h';子进程崩溃
        return 5;
    } else {
        printf("I am parent, pid = %d, mypid = %d\n", pid, getpid());
        int status;
        pid_t pid_recycle;
        //pid_t recycle=waitpid(-1,&status,0);
        //等价于wait()功能
        pid_recycle = waitpid(-1, &status, WHOHANG);
        //只能回收僵尸进程,返回僵尸进程的pid
        //通过查看man waitpid可知道-1,0,>0
        //WHOHANG没有子进程立即返回,不等待
        if (WIFEXITED(status))
        //宏操作,返回一个整型值,不是函数调用
        {
            printf("exit status = %d\n", WEXITSTATUS(status));
            //成功退出
        } else {
            printf("child process dump\n");
            //崩溃
        }
        printf("pid_recycle = %d\n", pid_recycle);
        //子进程的pid,被回收的进程pid
        return 0;
    }
}

2.进程的终止

<1>main函数的自然返回(return 0)

//该方法不能退出
void print()
{
    printf("I am print\n");
    return;
}

 

<2>调用exit函数//进程退出

//退出  echo $? //查看进程退出码  
//-1 就是255, -2 就是254,-3就是253
void print()
{
    printf("I am print\n");
    exit(-1);
    //不是返回值,是进程退出码(子进程的退出码)
}

 

<3>调用_exit函数

//退出
void print()
{
    printf("I am print\n");
    _exit(-1);
}

 

<4>调用abort函数

void print()
{
    	printf("I am print\n");
    	abort();
}

 

<5>接收到能导致进程终止的信号ctrl + c(SIGINT) ctrl + \ (SIGQUIT)

前三种方式为正常的终止,后两种为非正常终止,但是无论哪种方式,进程终止时都将执行相同的关闭打开的文件,释放占用的内存等资源,只是后两种终止会导致程序有些代码不会正常的执行比如对象的析构、atexit函数的执行

 

exit函数和_exit函数都是用来终止进程的,当程序执行到exit和_exit时,进程会无条件的停下剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本程序的运行。

 

exit函数和_exit函数的区别

exit函数和_exit函数的最大区别在于exit函数在退出之前会检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理I/O缓冲”。

清理I/O缓冲类似于调用fflush(stdout),而_exit()需要刷新stdout,同时printf为行缓冲

 

由于 linux 的标准函数库中,有一种被称作“缓冲 I/O”操作,其特征就是对应每一个

打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也为编程带来了麻烦。比如有一些数据,认为已经写入文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit 函数直接将进程关闭,缓冲区中的数据就会丢失。因此,如想保证数据的完整性,建议使用 exit 函数。

 

三、进程间打开文件的继承

1.用fork继承打开的文件

fork以后的子进程自动继承了父进程的打开的文件,继承以后,父进程关闭打开的文件不会对子进程造成影响。

 

2.守护进程

会话:会话组sid是bash的ID,一个进程不能加入别的会话,只能成立新的会话

pid_t setsid(void);//创建一个新的会话

syslog记日志也是守护进程,父进程是一号进程

概念:正常的进程当控制终端关闭时会自动关闭,但是守护进程从被执行开始运转直到整个系统关闭时才退出(几乎所有的服务器程序如apache和wu-ftp都用daemon进程的形式实现),守护进程最重要的特性是后台运行,成立新会话。

 

daemon进程的编程规则

1.创建子进程(fork),父进程退出(子进程变成孤儿进程)

2.在子进程中创建新会话(setsid-让进程摆脱原会话的控制,原进程组的控制,原控制终端的控制)

3.改变当前目录为根目录

4.重设文件权限掩码

5.关闭所有不需要的文件描述符(0,1,2)

6.添加守护进程任务

 

int setpgid(pid_t pid, pid_t pgid);//setpgid(0, 0);
//成立进程组,只能在本session内运行
pid_t getpgid(pid_t pid);//getpgid(0);
//获取进程组ID,参数为零,获取当前进程组id(父进程)
//ctrl + c 是发给进程组的,假如子进程自己成立进程组,
//则ctrl + c信号只能传给原来进程组
pid_t getsid(pid_t pid);//getsid(0);
//获取会话组id或会话周期id,参数0代表当前会话
pid_t setsid(void);//setsid();
//创建一个新的会话

int main()
{
    if (!fork())
    {
        setsid();//创建新会话
        chdir("/");//把当前工作目录切换为根目录
        umask(0);//重设文件权限掩码为零
        int i;
        for (int i = 0; i < 3; i++)
        //关闭所有不需要的文件描述符
        {
            close(i);
        }
        while(1);
    } else {
        exit(0);//父进程直接退出,创造僵尸子进程
    }
}

//最后用kill -9 daemon_pid命令直接杀掉守护进程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值