【Linux系统编程】进程

进程

文章目录

  • 进程
    • 1.进程概念
    • 2.创建进程函数fork
    • 3.进程实际运用场景
    • 4.vfork函数创建进程
    • 5.进程退出
    • 6.父进程等待子进程退出
      • 僵尸进程
      • wait函数
      • waitpid函数
      • 孤儿进程
    • 7.exec族函数
      • execl函数:
      • execlp函数:
      • execvp函数:
    • 8.linux 下修改环境变量配置绝对路径
    • 9.exec族函数配合fork函数使用
    • 10.system函数
    • 11.popen函数

1.进程概念

1.1什么是程序,什么是进程,有什么区别?

程序是静态的概念,gcc xxx.c -o pro

磁盘中生成的文件,叫做程序。

进程是程序的一次运行活动,通俗点的意思就是程序跑起来了,系统中就多了一个进程。

【Linux系统编程】进程_第1张图片

下面的这些程序,我们不打开它时,他就呆呆的待在桌面上,此时只是一个程序,如果双击运行起来,就开始执行,后台也会多了一个东西,它的名字就叫进程

【Linux系统编程】进程_第2张图片

【Linux系统编程】进程_第3张图片

1.2 如何查看系统中有那些进程

ps指令

实际工作中,配合grep来查找程序中是否存在某一个进程。(他会帮你过滤掉一些信息)
ps -aux|grep xxx;

在这里插入图片描述

top指令查看

类似windows的任务管理器

【Linux系统编程】进程_第4张图片

1.3什么是进程标识符

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

pid = 0 ;称为交换进程(swapper) 作用—进程的调度

pid = 1 ;init 进程 作用—系统的初始化

#include 
#include 

int main()
{
    pid_t pid;
    pid = getpid();
    printf("my pid is %d\n",pid);//并不是固定的,每次运行都会变,这点也要理解
    while(1);
    
    return 0;
}

【Linux系统编程】进程_第5张图片

1.4什么是父进程?什么是子进程?

进程 A 创建了进程 B

那么 A 叫做父进程,B 叫做子进程,父子进程是相对的概念,理解为人类中的父子关系

1.5C语言存储空间是如何分配的

【Linux系统编程】进程_第6张图片

【Linux系统编程】进程_第7张图片

2.创建进程函数fork

pid_t fork(void);

fork 函数会新生成一个进程,调用 fork 函数的进程为父进程,新生成的进程为子进程。
在父进程中返回子进程的 pid,在子进程中返回 0,失败返回-1。

fork 函数调用成功,返回两次,fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程pid;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值

通俗理解就是:fork是复制进程的函数,程序一开始就会产生一个进程,当这个进程(代码)执行到fork()时,fork就会复制一份原来的进程即就是创建一个新进程,我们称子进程,而原来的进程我们称为父进程,此时父子进程是共存的,他们一起向下执行代码。

注意:就是调用fork函数之后,一定是两个进程同时执行fork函数之后的代码,而之前的代码以及由父进程执行完毕。

2.1根据代码研究fork函数

#include 
#include 
 
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 id father print , pid = %d \n",getpid());
        }
        else{
                printf("this id child print , pid = %d \n",getpid());
        }
 
        return 0;
}

【Linux系统编程】进程_第8张图片

通过这个我们发现:

在调用fork函数之前,只有一个进程在跑,而调用了fork函数之后,原先的进程变为了父进程,紧接着下面代码的运行,而衍生出来的子进程,从调用它的代码后面接着运行,单独走一路与父进程分开(从本质上讲子进程是由父进程复制出来的产物,只是父进程调用了getpid得到了自己的进程ID号,子进程重复运行调用getpid得到属于子进程的ID号,由于进程ID号是唯一的,所以父进程与子进程的ID号并不相同,由此我们便通过if语句,使得两个进程走向不同的方向)。

2.2根据代码研究fork函数返回值

#include 
#include 
 
int main()
{
        pid_t pid;
        pid_t pid2;
        pid_t retpid;//做一个变量来承接fork函数返回值
  
        pid = getpid();
        printf("before fork: pid = %d \n",pid);
        retpid = fork();
        pid2 = getpid();//接收
        printf("after fork: pid = %d \n",pid2);
 
        if(pid == pid2){
                printf("this id father print , retpid = %d ,pid = %d\n",retpid,getpid());
        }
        else{
                printf("this id child print ,retpid = %d , pid = %d \n",retpid,getpid());
        }
        return 0;
}

【Linux系统编程】进程_第9张图片

**通过代码结果验证了开头说的结论:**在父进程中返回子进程的 pid,在子进程中返回 0,失败返回-1。

fork函数相当于拷贝了两次,一次给父进程,一次给子进程

注:fork创建了子进程,这时的子进程与父进程共享内存空间,只有当子进程的值发生改变时,与父进程不一样了,才会给子进程数据分配新空间。

3.进程实际运用场景

有一台服务器,每次客户端来介入,就需要服务器来处理这个客户端,谁来处理?fork函数创建的子进程,每一次客户端介入我就创建一个子进程来处理数据。

【Linux系统编程】进程_第10张图片

【Linux系统编程】进程_第11张图片

示例代码:

//我们用1来模拟客户端接入,进而处理一些我们想要的数据
#include 
#include 
 
int main()
{
    pid_t pid;
    int data = 10;

  	while(1){
        
      	printf("please input your data\n");
        scanf("%d",&data);
        if(data == 1)
        {
        	pid = fork();    
            if(pid>0){
               
            }else if(pid == 0){
                while(1){//不断的跟客户端去进行对接服务
                  	printf("do net request,pid=%d\n",getpid()); 
                    sleep(3);
                }
            }
        }else{
            printf("do nothing!\n");
        }
    }
    return 0;
}

【Linux系统编程】进程_第12张图片

4.vfork函数创建进程

  • 关键区别一:

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

  • 关键区别二:

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

vfork fork
子进程与父进程共享数据段. 子进程拷贝父进程的数据段,代码段
保证子进程先运行 父子进程的执行次序不确定
#include 
#include 
#include 
 
int main()
{
    pid_t pid;

    int cnt = 0;

    pid = vfork();

    if(pid > 0){
        while(1){
            printf("this is father print: pid = %d \n",getpid());
            printf("cnt = %d\n",cnt);
            sleep(1);
        }
    }
    else if(pid == 0){
        while(1){
            printf("this is child print: pid = %d \n",getpid());
            cnt++;
            if(cnt == 3){
                    exit(0);
                    //break;  这个属于异常退出了
            }
            sleep(1);
        }
    }

    return 0;
}

运行结果:【Linux系统编程】进程_第13张图片

  • 由vfork创造出来的子进程还会导致父进程挂起,除非子进程exit或者execve才会唤起父进程
  • 由vfok创建出来的子进程共享了父进程的所有内存,包括栈地址,直至子进程使用execve启动新的应用程序为止

从运行结果可以看到vfork创建出的子进程(线程)共享了父进程的cnt变量,二者的cnt指向了同一个内存,所以子进程修改了cnt变量,父进程的 cnt变量同样受到了影响。


冷知识:为什么会有vfork?

因为以前的fork当它创建一个子进程时,将会创建一个新的地址空间,并且拷贝父进程的资源,而往往在子进程中会执行exec调用,这样,前面的拷贝工作就是白费力气了,这种情况下,聪明的人就想出了vfork,它产生的子进程刚开始暂时与父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中运行,所以子进程不能进行写操作,

并且在儿子“霸占”着老子的房子时候,要委屈老子一下了,让他在外面歇着(阻塞),一旦儿子执行了exec或者exit后,相当于儿子买了自己的房子了,这时候就相当于分家了。此时vfork保证子进程先运行,在她调用exec或exit之后父进程才可能被调度运行。

因此vfork设计用以子进程创建后立即执行execve系统调用加载新程序的情形。在子进程退出或开始新程序之前,内核保证了父进程处于阻塞状态

用vfork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序,当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行,因为调用exec并不创建新进程,所以前后的进程id 并未改变,exec只是用另一个新程序替换了当前进程的正文,数据,堆和栈段。


5.进程退出

正常退出:

  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)作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination sratus)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态

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

还是上面的代码,子进程退出之后,父进程并没有收集子进程exit(0)返回来的数据。

僵尸进程

1.子进程退出状态不被收集,变成僵死进程(僵尸进程zombie)

#include 
#include 
#include 
 
int main()
{
    pid_t pid;
    int cnt = 0;
    pid = vfork();
    if(pid > 0){
        while(1){
            printf("this is father print: pid = %d \n",getpid());
            printf("cnt = %d\n",cnt);
            sleep(1);
        }
    }
    else if(pid == 0){
        while(1){
            printf("this is child print: pid = %d \n",getpid());
            cnt++;
            if(cnt == 3){
                    exit(0);
            }
            sleep(1);
        }
    }

    return 0;
}

【Linux系统编程】进程_第14张图片

wait函数

2.子进程退出状态被父进程收集,调用wait

我们为什么要用wait函数?

我们应当知道的是,在用fork创建子进程后,父子进程的执行的先后顺序是不定的,这时,我们可以用wait函数,wait()会暂停当前进程的执行,直到有信号到来或者子进程结束。总的来说,wait()的作用就是阻塞父进程,等待子进程。

  1. 我们是在父进程中使用wait(),可以不让父进程先于其产生的子进程结束,因为如果父进程结束了,而子进程还没有结束,那这个进程就会变成一个“孤儿进程”,(后面会讲)他会被init进程收养(进程号为1),并由init进程对它们完成状态收集工作。

  2. 当然如果子进程结束了,但是父进程没有调用wait或者waitpid(),那么子进程的资源就无法得到收集释放,这时候的子进程被称为“僵尸进程”,会占用系通资源,是非常不好的,危害很大。

总结:wait用于等待子进程结束,回收并释放子进程资源

wait函数

函数原型:pid_t wait(int *status)
头文件://不用加进代码里,已经被下面其他头文件包含了
#include 
#include 
    
函数参数:
1.status作为一个整形值,用来保存子进程退出时的状态;//exit(3)
2.如果status为NULL,表示忽略子进程退出时的状态;
3.如果status不为空,wait函数会将子进程退出的状态存入status中,
 另外,子进程退出时的状态可以通过linux中的特定的宏(macro)来进一步测定退出状态。

返回值:   
成功,返回子进程的进程号;
失败,返回-1

在等待的过程中:

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

wait函数status参数为空

//wait函数status参数为空
#include 
#include 
#include 
 
int main()
{
    pid_t pid;
    int cnt = 0;
    pid = vfork();
    if(pid > 0){
        wait(NULL);//唯一改动的地方,在子进程运行完之前,父进程会一直在这里等待子进程
        while(1){
            printf("this is father print: pid = %d \n",getpid());
            printf("cnt = %d\n",cnt);
            sleep(1);
        }
    }
    else if(pid == 0){
        while(1){
            printf("this is child print: pid = %d \n",getpid());
            cnt++;
            if(cnt == 3){
                    exit(0);
            }
            sleep(1);
        }
    }

    return 0;
}

运行结果:

【Linux系统编程】进程_第15张图片


wait函数status参数非空

当wait函数status参数非空,并且我们要查看返回的是哪一个子进程的终止状态的状态码的时候,我们就需要下面这个表来检查wait和waitpid所返回的终止状态的宏,来解析状态码。

说明
WIFEXITED(status) 若为正常终止子进程返回的状态,则为真。对于这种情况可执行WEXITSTATUS(status),取子进程传送给exit、_exit或_Exit参数的低8位
WIFSIGNALED(status) 若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这种情况,可执行WTERMSIG(status),取使子进程终止的信号编号。另外,有些实现定义宏WCOREDUMP(status),若已产生终止进程的core文件,则它返回真
WIFSTOPPED(status) 若为当前暂停子进程的返回状态,则为真。对于这种情况,可执行WSTOPSIG(status),取使子进程暂停的信号编号
WIFCONTINUED(status) 若在作业控制暂停后已经继续的子进程返回了状态,则为真。(POSIX.1的XSI扩展;仅用于waitpid。)

下面代码调用的是第一个宏 WEXITSTATUS(status) , 正常终止子进程返回的状态,为真的情况

//wait函数status参数非空
#include 
#include 
#include 
 
int main()
{
    pid_t pid;
    int cnt = 0;
    int status = 10;//提前定义出来
    pid = vfork();
    if(pid > 0){
        wait(&status);//函数参数是int型指针
        printf("child quit,child status = %d\n",WEXITSTATUS(status));//会打印出3,与exit有关
        while(1){
            printf("this is father print: pid = %d \n",getpid());
            printf("cnt = %d\n",cnt);
            sleep(1);
        }
    }
    else if(pid == 0){
        while(1){
            printf("this is child print: pid = %d \n",getpid());
            cnt++;
            if(cnt == 3){
                    exit(3);//值可以为0 1 2 3......
            }
            sleep(1);
        }
    }

    return 0;
}

运行结果:

【Linux系统编程】进程_第16张图片


waitpid函数

补充:waitpid函数

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

1.参数pid:

输入的pid 作用
pid > 0 等待其进程ID与pid相等的子进程。
pid = 0 等待其组ID等于调用进程组ID的任一子进程。
pid = -1 等待任一子进程。就这一方面而言,waitpid与wait等效。
pid < -1 等待其组ID等于pid绝对值的任一子进程。

2.参数options:

输入的options 作用
WNOHANG 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0
WUNTRACED 若某实现支持作业控制,而由pid指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告过,则返回其状态。WIFSTOPPED宏确定返回值是否对应于一个暂停子进程
WCONTINUED 若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态(POSIX.1的XSI扩展)
//这里我们用第一个options参数
//wait 使调用者阻塞,waitpid第一个选项,可以使调用者不阻塞。
#include 
#include 
#include 
 
int main()
{
    pid_t pid;

    int cnt = 0;
    int status = 10;

    pid = fork();

    if(pid > 0)
        //wait(&status);
        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());
            cnt++;
            if(cnt == 3){
                    exit(3);
            }
            sleep(1);
        }
    }
    return 0;
}

运行结果:【Linux系统编程】进程_第17张图片

我们看到父子进程都在同时跑,但子进程已经沦为僵尸进程了


孤儿进程

父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时的子进程叫做孤儿进程

Linux避免系统存在过多的孤儿进程,init进程(也就是干爹)(系统的一个初始化进程,它的pid号为1)收留孤儿进程,变成孤儿进程的父进程

#include 
#include 
#include 
 
int main()
{
    pid_t pid;

    int cnt = 0;
    int status = 10;

    pid = fork();

    if(pid > 0){
            printf("this is father print: pid = %d \n",getpid());//执行完,父进程死掉
    }
    else if(pid == 0){
        while(1){
            //getppid(),获取父进程的pid,了解一下
            printf("this is child print: pid = %d,my father pid = %d \n",getpid(),getppid());
            cnt++;
            if(cnt == 5){
                    exit(3);
            }
            sleep(1);
        }
    }
    return 0;
}

运行结果:在这里插入图片描述


7.exec族函数

date 获取系统时间 whereis ls 查找ls指令在那个路径下

1.exec族函数函数的作用:

我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。

exec族函数调用完不会往下走

2.功能:

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

3.函数族:

exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe

尾巴带e的两个函数不常用,入门不推荐,了解即可

4.函数介绍:

//1.头文件:
#include 
extern char **environ;

//2.函数原型:
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[]);

//3.参数说明:
path:可执行文件的路径名字(就是路径)
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
    
//4.返回值:
exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。

exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:

l 使用参数列表
p 使用文件名,并从PATH环境进行寻找可执行文件
v 应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e 多了envp[]数组,使用新的环境变量代替调用进程的环境变量

一、带l的一类exac函数(l表示list),包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。

execl函数:

//文件execl.c
#include 
#include 
#include 
//函数原型: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;
}
//文件echoarg.c
//编译该文件,gcc echoarg.c -o echoarg  生成一个名为echoarg的可执行文件
#include 

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;
}

运行结果:

【Linux系统编程】进程_第18张图片

实验说明:
我们先用gcc编译echoarg.c,生成可执行文件echoarg并放在当前路径bin目录下。文件echoarg的作用是打印命令行参数。然后再编译execl.c并执行execl可执行文件。用execl 找到并执行echoarg,将当前进程main替换掉,所以”after execl” 没有在终端被打印出来。


加餐:

在上述运行结果的第三步中,如果找不到这个程序,会返回-1,但是我们并不知道错误的原因是什么?如何知道?

//文件execl2.c
#include 
#include 
#include 

int main(void)
{
    printf("before execl\n");
    if(execl("./echoarg","echoarg","abc",NULL) == -1)
    {
        printf("execl failed!\n"); 
        perror("why");//用这个perror函数解析出来,这里不细讲了
    }
    printf("after execl\n");
    return 0;
}

运行结果:【Linux系统编程】进程_第19张图片

因为我们并没有在./echoarg这个路径中生成echoarg执行程序文件,所以报错了

再加一餐:

Linux指令—ls,也可以说是一个可执行程序(打印出来当前文件下的文件)如何通过代码来调用ls?

首先根据execl函数参数可知,第一个参数为路径,我们就需要知道ls在那个根目录下,如何查找?whereis ls

在这里插入图片描述

//文件execl3.c
#include 
#include 
#include 

int main(void)
{
    printf("before execl\n");
    // ./bin中的点是当前路径下,所以不能加点
    if(execl("/bin/ls","ls",NULL,NULL) == -1)//第三个参数暂时先为空,我们看一下运行结果
    {
        printf("execl failed!\n"); 
        perror("why");//用这个perror函数解析出来,这里不细讲了
    }
    printf("after execl\n");
    return 0;
}

运行结果:【Linux系统编程】进程_第20张图片

拓展:

指令ls -l 显示长列表文件【Linux系统编程】进程_第21张图片

-l如何输入?用execl函数的第三个参数

//文件execl4.c
#include 
#include 
#include 

int main(void)
{
    printf("before execl\n");
    // ./bin中的点是当前路径下,所以不能加点
    if(execl("/bin/ls","ls","-l",NULL) == -1)//第三个参数暂时先为空,我们看一下运行结果
    {
        printf("execl failed!\n"); 
        perror("why");//用这个perror函数解析出来,这里不细讲了
    }
    printf("after execl\n");
    return 0;
}

运行结果:与ls -l同效

【Linux系统编程】进程_第22张图片

指令:date显示系统当前时间

execlp函数:

在第一个函数传参过程中,不需要再加绝对路径了,这个函数自己会在**PATH环境变量**中寻找可执行文件

#include
#include

int main(void)
{
   printf("before execlp\n");

   if(execlp("ps","ps",NULL,NULL)==-1)//唯一的变化就是不需要路径了
   {
        printf("execl failed\n");
        perror("why");
   } 

   printf("afther execlp\n");

   return 0;
}

execvp函数:

这个相当于啥呢,就是在execlp函数的基础上,把二三四参数搞到了一个字符型指针数组里面了

#include
#include

int main(void)
{
   printf("before execlvp\n")   
   char *argv[]={"ps",NULL,NULL};
   if(execvp("ps",argv)==-1)
   {
        printf("execlvp failed\n");
        perror("why");
   } 
   printf("afther execlvp\n");
   return 0;
}

8.linux 下修改环境变量配置绝对路径

当前环境变量PATH查看:echo $PATH (系统可以找到这个路径底下的可执行程序)

在这里插入图片描述

我们在编译程序时往往需要 ./a.out 来运行这个程序,“ ./ ”表明是在当前路径下,而你有木有好奇为什么像 ls cd cp 这些Linux指令,同样是可执行程序,为什么他们就不是 “ ./ls ”?原因就是这些指令全部在PATH环境变量里?

那么只要我们能把程序路径也搞到环境变量里面去,以后就再也不用“ ./a.out ”了,说搞就搞

第一步:

Linux命令:pwd 查看当前路径是多少

在这里插入图片描述

第二步:

Linux命令:export PATH=$PATH:所要加的路径

【Linux系统编程】进程_第23张图片

9.exec族函数配合fork函数使用

实现功能:当父进程检测到输入为1的时候,创建子进程把配置文件的字段值修改掉。

文件:

//文件名:config.txt
SPEED=5		
LENG=1		
SCORE=90           
LEVEL=95

1.修改配置文件的程序:

详细介绍请看Linux系统编程中的文件编程

//文件名 a.c 编译后执行文件 a
#include
#include
#include
#include
#include
#include
#include

void findFun(char *str1,char *str2)
{
     char *p=strstr(str1,str2);
     if(p==NULL)
     {
         printf("no found\n");
         exit(-1);
     }
     p=p+strlen(str2);//指针偏移到需要改动的地方
     *p='5';//注意是字符5
}

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

    char *readBuf=NULL;

    if(argc!=2)
    {
        printf("the params no ok\n");
        exit(-1);
    } 

    fdSrc=open(argv[1],O_RDWR);
    
    size=lseek(fdSrc,0,SEEK_END);//计算出配置文件的大小
    lseek(fdSrc,0,SEEK_SET);//把光标移到头,为下面读操作做铺垫
    readBuf=(char*)malloc(sizeof(char)*size+1); 
    read(fdSrc,readBuf,size);
    findFun(readBuf,"LENG=");    
    
    lseek(fdSrc,0,SEEK_SET); //移到头,把原先信息覆盖掉 
/******************方法二:****************
    close(fdSrc);
    open("./file1",O_RDWR|O_TRUNC);
*****************************************/
    write(fdSrc,readBuf,strlen(readBuf));  
    close(fdSrc);
    return 0;
}

2.fork函数配合exec调用a.c程序:

//b.c
//我们用1来模拟客户端接入,进而处理一些我们想要的数据
#include 
#include 
 
int main()
{
    pid_t pid;
    int data = 10;

  	while(1){
      	printf("please input your data\n");
        scanf("%d",&data);
        if(data == 1)
        {
        	pid = fork();    
            if(pid>0){
               wait(NULL);
            }else if(pid == 0){ 
                //if(execl("./a.c","a","config.txt",NULL) == -1)不要加后缀名.c
                if(execl("./a","a","config.txt",NULL) == -1)
                {
                    printf("execl failed!\n");      
                }else{
                    printf("execl sucessful!\n");//注意这个点,execl调用成功并不会执行到这里
                }
            }
        }else{
            printf("do nothing!\n");
        }
    }
    return 0;
}

运行结果:

我们看到execl成功被调用后并不会再向后运行(小本本记重点,后面要与system函数做区别)

【Linux系统编程】进程_第24张图片

10.system函数

//1.头文件
#include
//2.函数原型
int system(const char * string);
函数说明
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行
完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。
//3.返回值
1.成功,则返回进程的状态值;
2.当sh不能执行时,返回1273.失败返回-1;
如果system()在调用/bin/sh时失败则返回127,其他失败原因返回-1。若参数string为空指针(NULL),则返回非零值。
如果 system()调用成功则最后会返回执行shell命令后的返回值,但是此返回值也有可能为system()调用/bin/sh失败
所返回的127,因此最好能再检查errno 来确认执行成功。

附加说明:
在编写具有SUID/SGID权限的程序时请勿使用system(),system()会继承环境变量,通过环境变量可能会造成系统安全的问题。

当system接受的命令为NULL时直接返回,否则fork出一个子进程,因为fork在两个进程:父进程和子进程中都返回,这里要检查返回的 pid,fork在子进程中返回0,在父进程中返回子进程的pid,父进程使用waitpid等待子进程结束,子进程则是调用execl来启动一个程序代替自己,execl(“/bin/sh”, “sh”, “-c”, cmdstring,(char*)0)是调用shell,这个shell的路径是/bin/sh,后面的字符串都是参数,然后子进程就变成了一个 shell进程,这个shell的参数是cmdstring,就是system接受的参数。在windows中的shell是command,想必大家很熟悉shell接受命令之后做的事了。

如果上面的你没有看懂,那我再解释下fork的原理:当一个进程A调用fork时,系统内核创建一个新的进程B,并将A的内存映像复制到B的进程空间中,因为A和B是一样的,那么他们怎么知道自己是父进程还是子进程呢,看fork的返回值就知道,上面也说了fork在子进程中返回0,在父进程中返回子进程的pid。(上述篇章皆有讲解)

system函数源码:

//说白了,sysytem函数就是将execl函数给封装了起来
int system(const char * cmdstring)
{
  pid_t pid;
  int status;

  if(cmdstring == NULL){
      
      return (1);
  }


  if((pid = fork())<0){

        status = -1;
  }
  else if(pid == 0){
    execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);//(char *)0 就是NULL空指针的意思
    -exit(127); //子进程正常执行则不会执行此语句
    }
  else{
        while(waitpid(pid, &status, 0) < 0){
          if(errno != EINTER){
            status = -1;
            break;
          }
        }
    }
    return status;
}

这两个指令其实是等效的,只不过习惯于把sh -c ls省略了

【Linux系统编程】进程_第25张图片

下面简单做一个程序demo:

//把系统时间打印出来
#include 
#include 

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

	if(system("date") == -1){
		printf("execl filed!\n");
		
		perror("becasue");
	}
	printf("system successful\n");
	return 0;
}

运行结果:我们发现system在调用完之后,还会继续再向下运行,这就是与execl函数相区别的地方(一般我们更倾向于用system

system( ) 函数的参数书写的规律是,可执行文件怎么执行,就怎么写:比如 system(“./a.out aa bb”);

在这里插入图片描述

11.popen函数

提到system函数,就不得不提到popen函数,根据system函数的源代码:system函数的执行需要通过调用fork()函数创建一个子进程,子进程通过execl函数调用shell对传参的可执行文件进行实现。这也意味着system函数实现需要依赖execl函数实现自身功能。因此system函数的结果将直接显示在终端上,这样原本运行的结果就无法保存在文件中用于实现信息交互等功能。

//1.头文件
#include 
//2.函数原型
FILE *popen(const char *command, const char *type);

int pclose(FILE *stream);

参数说明:
commmand:是一个指向以 NULL 结束的 shell 命令字符串的指针。
 		  这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。

type:只能是读和写的一种,如果是 “r” 则文件指针连接到command的标准输出,
      则返回的文件指针是可读的;如果是 “w” 则文件指针连接到command的标准输入,则返回的文件指针是可写的。

stream:popen返回的文件指针。
 
//3.返回值
如果调用fork()pipe()失败,或者不能分配内存将返回NULL,否则返回标准I/O流。
popen()没有为内存分配失败设置errno值。如果调用fork()pipe()时出现错误,errno被设为相应的错误类型。
如果type参数不合法,errno将返回EINVAL。

popen函数比system在实际应用中的好处:可以获得运行的输出结果。

**作用:**创建一个连接到另一个进程的管道,然后读其输出或向其输入端发送数据。

**原理:**创建一个管道,fork一个子进程,关闭未使用的管道端(读端或者写端),执行一个shell运行命令,然后等待命令终止。

示例程序:

#include 
#include 
#include 
 
//FILE *popen(const char *command, const char *type);
 
//size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
 
int main(void)
{
        FILE *fp;
        char ret[1024]={0};
 
        fp = popen("ps","r");   
        int nread = fread(ret,1,1024,fp);
 
        printf("read ret %d byte,ret = %s \n",nread,ret);
 
        return 0;
}

运行结果:【Linux系统编程】进程_第26张图片

如果没有上述打印的内容,实验结果将毫无现象。因此可以通过popen函数读入信息,通过网络的方式发送从而实现多机通信。

pclose函数则用于关闭标准I/O流。如果使用popen函数在 “w” 模式下,则可以继续往标准I/O流中写入内容,直到调用pclose关闭它。

你可能感兴趣的:(#,进程,Linux系统编程,linux,运维,服务器,c语言,数据结构,笔记,经验分享)