Linux教程

Linux系统编程学习笔记——进程

本文主要是介绍Linux系统编程学习笔记——进程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

进程相关概念

1.什么是程序,什么是进程

程序:一般是放在实体磁盘中,通过使用者的运行来触发,出发后会加载到内存中成为一个个体,这就是程序。

程序(program):通常为二进制,放置在储存媒体中(如光盘、硬盘、软盘、磁带等),为实体文件的形态存在。

进程(process):程序触发后,运行者的权限与属性、程序的程序码和所需数据等都会被加载到内存中,操作系统给予这个内存单元一个识别码(PID),可以说,进程就是一个运行中的程序。

2.如何查看系统中有哪些进程

a.使用ps指令查看,实际工作,配合grep来查找程序中是否存在一某一个进程

b.使用top指令查看,类似windows任务管理器

3.什么是进程标识符

每一个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证

 Pid=0:称为交换进程(swapper),作用是进程调度

Pid=1:init进程,作用是进程初始化

编程调用getpid函数获取自身的进程标识符

getppid获得父进程的进程标识符

#include <stdio.h>
#include <sys/types.h>
#include <unisted.h>

int main()
{
    pid_t pid;
    
    pid = getpid();
    
    printf("my pid is %d\n",pid);
    return 0;
}

4.父进程和子进程

进程A创建了进程B

那么A叫做父进程,B叫做子进程,父子进程是先对的概念.

5.C程序的存储空间是如何分配的

正文(代码段):是由CPU执行的机器指令部分。通常,正文段时可共享的,所以即使是频繁执行的程序(如文本编辑器,C编译器和shell等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由意外而修改其中自身的指令。

初始化数据段:通常将此段称为数据段,它包含了程序中需明确地赋初值的变量。例如,C程序中出现在任何函数之外的声明:

int maxcount = 99;

使此变量带有其初值放在初始化数据段中。

非初始化数据段:通常将此段称为bss段,在程序开始执行前,内核将此段中的数据初始化为0或空指针。出现在任何函数外的C声明:

long sum[1000]; 

使此变量存放非初始化数据中。

:自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次调用函数时,其返回地址以及调用者的环境信息(例如某些及前期寄存器的值)都放在栈中。然后,最近被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,可以递归调用C函数。递归函数每次调用自身时,就使用一个新的栈帧,因此一个函数调用实例中的变量集不会影响另一个函数调用实例中的变量集。

:通常在堆中进行动态分配。

7.创建进程fork的使用

fork()系统调用

【功能】创建一个新的子进程

【调用格式】 int fork();

【返回值】0 向子进程返回的返回值,总为0

>0 向父进程返回的返回值,它是子进程的PID

-1 创建失败

【说明】若fork()调用成功,则它向父进程返回子进程的PID,并向新建的子进程返回0。

例1:

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


int main()
{
        pid_t pid;
        pid_t pid2;

        pid = getpid();
        printf("before fork pid = %d\n",pid);

        fork();

        pid2 = getpid();
        printf("after fork pid = %d\n",pid2);

        if(pid == pid2)
        {
                printf("this is father print\n");
        }else{
                printf("this is child print,child pid = %d\n",getpid());
        }


        return 0;
}

例2:

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


int main()
{
        pid_t pid;

        printf("father pid = %d\n",getpid());

        pid = fork();

        if(pid > 0)
        {
                printf("this is father print,pid = %d\n",getpid());
        }
        else if(pid == 0)
        {
                printf("this is child print,pid = %d\n",getpid());
        }

        return 0;
}

fork创建一个子进程的目的:

        (1)一个父进程希望复制自己,使父,子进程同时执行不同的代码段。这在网路服务进程中是常见的——父进程等待客户端的服务请求。当这种情求达到时,父进程调用fork(),使子进程处理此请求,父进程则继续等待下一个服务请求到达。

        (2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种状况下,子进程从fork返回后立即调用exec。

总结: 

        由fork创建的新进程称为子进程(child process)。fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,父进程的返回值是子进程的PID。

        将子进程PID返回给父进程的原因:因为一个进程的子进程可以有多个,且没有一个函数使一个进程可以获得所有的子进程的PID。

        fork使子进程得到返回值0的理由:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得父进程的PID。

        子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程的数据空间、堆和栈的副本。注意,这是子进程所拥有的副本,父,子进程并不共享这些存储空间,父,子进程共享正文段。

        由于在fork之后经常跟随exec,所以现在的很多实现并不执行一个父进程数据段、堆和栈的完全复制。作为代替,使用了写时复制技术。这些区域由父,子进程共享,而且内核将它们的访问权限改为只读。如果父,子进程中的任意一个试图修改这些区域,则内核只为修改区域那块内存制作一个副本,通常是虚拟存储系统中的一“页”。

8.vfork函数

与fork的区别:

1.vfork直接使用父进程存储空间,不拷贝。

2.vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。

例1:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        pid_t pid;

        pid = vfork();

        if(pid > 0){
                while(1){
                        printf("this is father print, pid = %d/n",getpid());
                        sleep(1);
                }

        }
        else if(pid == 0){
                while(1){
                        printf("this is child print,pid = %d\n",getpid());
                        sleep(1);
                }
        }

        return 0;
}

可以看到,如果不调用exit,子进程会一直执行。

例2:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        pid_t pid;

        int cnt = 0;

        pid = vfork();

        if(pid > 0){
                while(1){
                        printf("cnt=%d\n",cnt);
                        printf("this is father print, pid = %d/n",getpid());
                        sleep(1);
                }

        }
        else if(pid == 0){
                while(1){
                        printf("this is child print,pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(0);
                        }
                }
        }

        return 0;
}


9.进程退出

正常退出

1.Main函数调用return

2.进程调用exit(),标准C库

3.进程调用_exit() 或者_Exit(),属于系统调用

补充:

1.进程最后一个线程返回

2.最后一个线程调用pthread_exit

异常退出

1.调用abort

2.当进程收到某些信号时,如ctrl+c

3.最后一个线程对取消(cancellation)请求做出响应

        不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它们所使用的存储器。

        对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的,对于三个终止函数(exit、_exit、_Exit)实现这一点的方式是,将其退出状态(exit status)作为参数返回给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止的原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait或者waitpid函数取得其终止状态。

10.父进程等待子进程退出

(1)为什么要等待子进程退出

        创建子进程得目的是为了让子进程干别的活,设置退出状态的作用是通知父进程有关此次运行的状况,以便父进程做出相应的处理,等待子进程得退出,收集退出状态。

(2)父进程如何等待子进程退出状态并收集子进程得退出状态

         子进程退出状态不被收集,则变为僵尸进程。

例1:僵尸状态演示

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        pid_t pid;

        int cnt = 0;

        pid = fork();

        if(pid > 0)
        {
                while(1){
                        printf("cnt=%d\n",cnt);
                        printf("this is father print, pid = %d\n",getpid());
                        sleep(1);
                }

        }
        else if(pid == 0){
                while(1){
                        printf("this is child print,pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(0);
                        }
                }
        }

        return 0;
}

        通过运行结果可以看到,父子进程是并发执行的,子进程结束后退出,父进程并没有调用wait()收集子进程的状态,此时用ps -aux|grep 查看,子进程的状态是z+,说明子进程此时是僵尸状态。

wait()系统调用

        【功能】阻塞进程直到子进程结束;收集子进程。

        【调用格式】int wait(int *status)

        【参数说明】*status保存了子进程的一些状态。如果是正常退出,则字节莫为0,第2字节为退出状态;如果是非正常退出(即被某个信号终止),则其末字节不为0,末字节的低7位为导致进程终止的信号的信号值,若不关心子进程是如何终止的,可以用NULL作参数,即wait(NULL)。

        【返回值】>0 子进程的PID;

        -1 调用失败;

        0 其他;

  • 如果所有子进程都还在运行,则阻塞
  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
  • 如果它没有任何子进程,则立即出错返回

区别:wait使调用者阻塞,waitpid有一个选项,可以使调用者不阻塞。

waitpid()系统调用

        【功能】与wait()一样

        【调用格式】pid_t waitpid(pid_t pid, int *status, int options);

        【参数说明】

  •         对于waitpid函数中pid参数的说明:
pid == -1等待一子进程
pid > 0等待其进程ID与pid相等的子进程
pid == 0等待其组ID等于调用进程组ID的任一子进程
pid < 0等待其ID等于pid绝对值的任一进程
  •         status参数与wait()一样
  • waitpid的options常量
常量说明
WCONTINUED若实现支持作用控制,那么由pid指定的任一子进程在暂停后已经继续,但状态尚未报告,则返回其状态
WNOHANG若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0
WUNTRACED若某实现支持作业控制,而pid指定的任一子进程已进入状态状态,并且其状态自暂停以来从未报告过,则返回其状态。WIFSTOPPED宏确定返回值是否对应一个暂停子进程

例2:调用wait()后

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        pid_t pid;

        int cnt = 0;

        pid = fork();

        if(pid > 0)
        {
                        wait(NULL);
                        while(1){
                        printf("cnt=%d\n",cnt);
                        printf("this is father print, pid = %d\n",getpid());
                        sleep(1);
                }

        }
        else if(pid == 0){
                while(1){
                        printf("this is child print,pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(0);
                        }
                }
        }

        return 0;
}

父进程等待子进程运行结束后再运行,并收集子进程的退出状态,用 ps -aux|grep 查看可以看到子进程退出后,子进程的PID消失了

例子3:调用waitpid()

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        pid_t pid;

        int cnt = 0;
        int status = 10;

        pid = fork();

        if(pid > 0)
        {
                        waitpid(pid,&status,WNOHANG);
                        printf("child quit, child status =%d\n",WEXITSTATUS(status));
                        while(1){
                        printf("cnt=%d\n",cnt);
                        printf("this is father print, pid = %d\n",getpid());
                        sleep(1);
                }

        }
        else if(pid == 0){
                while(1){
                        printf("this is child print,pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(3);
                        }
                }
        }

        return 0;
}

注意:虽然父进程调用waitpid()来收集子进程的退出状态,但使用 ps - aux|grep 查看发现,非阻塞等待子进程也会变成僵尸进程

 例子4:检查退出状态      

        如果检查退出状态,status就不能是NULL,而是wait(&status);

        如果要检查exit()中的返回值,需要使用检查wait和waitpid所返回的终止状态的宏 

说明
WIFEXITED(status)若为正常终止子进程返回的状态,则真。可执行WEXITSTATUS(status),取子进程传送给exit,_exit或_Exit参数的第8位
WIFSIGNALED(status)若为异常终止子进程返回的状态,则为真。可执行WTERMSIG(status),取使子进程终止的信号编号。另外,有些实现定义宏WCOREDIUMP(status),若以产生终止进程的core文件,则它返回真
WIFSTOPPED(status)

若为当前暂停子进程返回状态,则为真。对于这种情况,可执行WSTOPSIG(status0),取使子进程暂停的信号编号

WIFCONTUNUED(status)若在作业控制暂停后以及继续的子进程返回状态,则为真。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
        pid_t pid;

        int cnt = 0;
        int status = 10;

        pid = fork();

        if(pid > 0)
        {
                        wait(&status);
                        printf("child quit, child status =%d\n",WEXITSTATUS(status));
                        while(1){
                        printf("cnt=%d\n",cnt);
                        printf("this is father print, pid = %d\n",getpid());
                        sleep(1);
                }

        }
        else if(pid == 0){
                while(1){
                        printf("this is child print,pid = %d\n",getpid());
                        sleep(1);
                        cnt++;
                        if(cnt == 3){
                                exit(3);
                        }
                }
        }

        return 0;
}

11.孤儿进程

        父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程。Linux避免系统存在过多孤儿进程,init进程会收留孤儿进程,变成孤儿进程的父进程。由于init进程不会退出,所以所有的进程都会被收养,最后,在系统关机之前,init进程要负责结束所有的进程。

12.exec族函数

大佬总结的文章:

linux进程---exec族函数(execl, execlp, execle, execv, execvp, execvpe)_牛仔的blog-CSDN博客_execlehttps://blog.csdn.net/u014530704/article/details/73848573

为什么要用exec族函数,有什么作用

        (1)一个父进程希望复制自己,使父,子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的请求。当这种请求达到时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。

        (2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。

exec族函数的定义

        【功能】在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本

        【函数族】exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe
        【函数原型】

        

#include <unistd.h>
extern char **environ;

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[]);

        【返回值】exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。

        【参数说明】

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

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

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

例子:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//函数原型:int execl(const char *path,const char *arg,...);


int main(void)
{
        printf("before execl\n");
        if(execl("./echoarg","echoarg","abc",NULL) == -1)
        {
                printf("execl failed!\n");
        }
        printf("after execl\n");

        return 0;
}
#include <stdio.h>

int main(int argc,char *argv[])
{
        int i = 0;
        for(i=0;i<argc;i++)
        {
                printf("argv[%d] :%s\n",i,argv[i]);
        }

        return 0;
}

        先gcc编译echoarg.c,生成可执行文件echoarg。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。

        exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。当发生错误时可以用 perror 解析错误。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
//int execl(const char *path,const char *arg,...);


int main(void)
{
        printf("before execl\n");
        if(execl("./bin/echoarg","echoarg","abc",NULL) == -1)
        {
                printf("execl failed!\n");

                perror("why");
        }
        printf("after execl\n");
        
        return 0;
}

 

这里说明我们的路径设置错误,echoarg就在当前目录下,没有在bin目录。

whereis 可以查看文件的路径

 

13.exec族函数配合fork使用

例:配置文件的修改

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

int main()
{
        pid_t pid;
        int data = 10;

        while(1){
                printf("please input a data\n");
                scanf("%d",&data);
                if(data == 1){
                                int fdSrc;
                                pid =fork();
                                if(pid > 0){
                                        wait(NULL);
                                }
                                if(pid == 0){
                                        execl("./changedata","changedata","config.txt",NULL);
                                        exit(0);
                                }
                }
                else{
                        printf("wait,do nothing\n");
                }
        }

        return 0;
}

changedata 文件:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(int argc,char **argv)
{
        int fdSrc;

        char *readBuf = NULL;

        fdSrc = open("config.txt",O_RDWR);

        int size = lseek(fdSrc,0,SEEK_END);
        lseek(fdSrc,0,SEEK_SET);

        readBuf=(char*)malloc(sizeof(char)*size + 8);

        int n_read = read(fdSrc,readBuf,1024);

        char *p = strstr(readBuf,"LENG=");
        if(p==NULL){
                printf("not found\n");
                exit(-1);
        }

        p = p+strlen("LENG=");
        *p = '5';

        lseek(fdSrc,0,SEEK_SET);

        int n_write = write(fdSrc,readBuf,strlen(readBuf));

        close(fdSrc);
    return 0;
}

 使用execl()使代码简洁了许多。

        

这篇关于Linux系统编程学习笔记——进程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!