多进程编程及相关函数

文章目录

  • 查看系统中的进程
  • 进程标识
  • 进程创建
  • 进程终止
  • 僵尸进程
  • 守护进程和孤儿进程
  • wait函数
  • exec函数
  • system函数

程序是存放在磁盘文件中的可执行文件。程序的执行实例被称为进程,进程具有独立的权限与职责。
每个进程运行在其各自的虚拟地址空间中,进程之间可以通过由内核控制的机制相互通讯。如果系统中某个进程崩溃,它不会影响到其余的进程。
每个Linux进程都一定有一个唯一的数字标识符,称为进程ID,PID(进程ID)总是一个非负整数。
在进程的main函数执行之前内核会启动,编译器在编译的时候会将启动例程编译进可执行文件中。
启动例程的作用:搜集命令行的参数传递给main函数中的argc和argv;搜集环境信息构建环境表并传递给main函数;登记进程的终止函数。

查看系统中的进程

通过下面的命令就可以查看当前系统执行的进程。

ps
ps -ef
ps -aux
ps -ef | more

当前系统执行的进程如下图所示。
在这里插入图片描述
USER是指进程的属主;PID:进程ID号;PPID:父进程ID号;%CPU:进程占用的CPU百分比;%MEM:占用内存的百分比;VSZ:进程虚拟大小;RSS:驻留中页的数量;TTY:终端ID;STAT:进程的状态;START:启动进程的时间;TIME:进程消耗CPU的时间;COMMAND:命令的名称和参数。
进程常见的状态:运行状态®、等待状态(S)、停止状态(T)、僵尸状态(Z)。僵尸状态指的是进程终止或结束,但是在进程表项中仍有记录。


进程标识

获取进程相关标识的函数原型如下。

#include <unistd.h>
#include <sys/types.h>
pid_t getpid(void);   //获取当前进程ID
uid_t getuid(void);   //获得当前进程的实际用户ID
uid_t geteuid(void);   //获得当前进程的有效用户ID
gid_t getgid(void);   //获得当前进程的用户组ID
pid_t getppid(void);   //获得当前进程的父进程ID
pid_t getpgrp(void);   //获得当前进程所在的进程组ID
pid_t getpgid(pid_t pid);  //获得进程ID为pid的进程所在的进程组ID

关于进程标识相关的代码示例如下。

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

int main()
{
    printf("pid : %d\n",getpid());
    printf("ppid : %d\n",getppid());
    printf("uid : %d\n",getuid());
    printf("euid : %d\n",geteuid());
    printf("user gid : %d\n",getgid());
    printf("gid : %d\n",getpgrp());
    printf("pgid : %d\n",getpgid(getpid()));
    printf("ppgid : %d\n",getpgid(getppid()));
    return 0;
}

程序编译之后的执行结果如下图所示。
在这里插入图片描述


进程创建

进程创建使用的头文件及函数原型如下。

#include <unistd.h>
#include <sys/types.h>
pid_t fork(void); 
pid_t vfork(void);

进程创建函数调用第一,返回两次,在子进程中返回值为0,在父进程中返回值是子进程的进程ID号。
使用进程创建函数fork()创建子进程,父子进程哪个先运行根据系统调度决定,子进程会复制(继承)父进程的内存空间。
使用进程创建函数vfork()创建子进程,子进程先运行且不复制父进程的内存空间。
子进程继承父进程的属性包括:用户信息和权限、目录信息、信号信息、环境、共享存储段、资源限制、堆、栈和数据段,代码段是父子进程共享的
子进程的特有属性:进程ID、锁信息、运行时间、未决信号。
操作文件时的内核结构变化:子进程只继承父进程的文件描述表,不继承但共享文件表项和i-node;父进程创建一个子进程后,文件表项中的引用计数器加1变成2,当父进程作close操作后,计数器减1,子进程还是可以使用文件表项,只有当计数器为0时才会释放文件表项。
使用fork函数创建进程的代码示例如下。

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

int main()
{
    pid_t pid;
    printf("当前运行进程的ID : %d\n",getpid());
    pid = fork();
    if(pid < 0)
        perror("Error!\n");
    else if(pid == 0)
        printf("子进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
    else
        printf("父进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
    printf("当前运行进程的ID : %d\n",getpid());
    sleep(1);
    return 0;
}

上面程序编译后运行结果如下图所示。
在这里插入图片描述
可以清楚的看到,在进程创建完毕后,系统中就有了两个进程,一句打印输出的代码被父子进程各执行了一次!
父进程和通过fork()函数创建出的子进程有相同的虚拟空间,但是却有各自的物理空间!
在这里插入图片描述
下面的例子就能够说明这一点。

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

int global_v = 10;
int main()
{
    int area_v = 10;
    static int static_v = 10;
    pid_t pid;
    printf("当前运行进程的ID : %d\n",getpid());
    pid = fork();
    if(pid < 0)
        perror("Error!\n");
    else if(pid == 0)
    {
        global_v = 20;
        area_v = 20;
        static_v = 20;
        printf("子进程中,global_v地址:%p area_v地址:%p static_v地址:%p\n",&global_v,&area_v,&static_v);
        printf("子进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
    }
    else
    {
        global_v = 30;
        area_v = 30;
        static_v = 30;
        printf("父进程中,global_v地址:%p area_v地址:%p static_v地址:%p\n",&global_v,&area_v,&static_v);
        printf("父进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
    }
    printf("当前运行进程的ID : %d, global_v = %d, area_v = %d, static_v = %d\n",getpid(),global_v,area_v,static_v);
    return 0;
}

程序编译后的运行结果如下图所示。
在这里插入图片描述
可以看到,在父子进程中打印的变量地址是一样的(这里打印的变量地址是虚拟地址,并非物理地址),但是里面存放的值却是不一样的,这说明父子进程有相同的虚拟空间,却各自映射了独立的物理空间。
关于虚拟空间和物理空间,下面是一个例子。

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

int global_v = 10;
int main()
{
    int area_v = 10;
    static int static_v = 10;
    pid_t pid;
    FILE *fp = fopen("a.txt","w");   //带缓存
    int fd = open("b.txt",O_WRONLY|O_CREAT|O_TRUNC,S_IRWXU|S_IRWXG);  //不带缓存
    char *s = "helloworld!";
    ssize_t size = strlen(s)*sizeof(char);
    fprintf(fp,"字符串s : %s   pid : %d\n",s,getpid());   //标准IO函数,带缓存,先写入缓存,再写入文件
    write(fd,s,size);   //内核提供的IO系统调用,不带缓存,直接写入文件
    pid = fork();
    if(pid < 0)
        perror("Error!\n");
    else if(pid == 0)
    {
        global_v = 20;
        area_v = 20;
        static_v = 20;
        printf("子进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
    }
    else
    {
        global_v = 30;
        area_v = 30;
        static_v = 30;
        printf("父进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
    }
    fprintf(fp,"当前运行进程的ID : %d, global_v = %d, area_v = %d, static_v = %d\n",getpid(),global_v,area_v,static_v);  
    //写入父子进程各自的缓存中,然后清缓存并写入文件中
    fclose(fp);
    close(fd);
    return 0;
}

程序编译后的执行结果如下图所示。
在这里插入图片描述
fork()生成的子进程会将父进程中的缓存复制一份,就像上面图中划线的两行,内容是完全相同的,此后,父子进程再写入缓存中各自的部分,最后一起写入一个文件中。系统调用是没有缓存的,内容是直接写入文件的。
子进程会继承父进程的文件描述符,下面是父进程将文件偏移量移动到文件尾,再由子进程来追加信息的例子。

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

int main(int argc,char *argv[])
{
    if(argc < 2)
    {
        fprintf(stderr,"usage : %s filename\n",argv[0]);
        exit(1);
    }
    int fd = open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,S_IRWXU|S_IRWXG);
    if(fd < 2)
    {
        perror("open error");
        exit(1);
    }
    pid_t pid = fork();
    if(pid < 0)
        perror("fork error");
    else if(pid > 0)
    {
        char *s = "Parent process input!\n";
        ssize_t size = strlen(s)*sizeof(char);
        if(write(fd,s,size) != size)
        {
            perror("write error");   
            exit(1); 
        }
        else
            printf("Parent process write success!\n"); 
        if(lseek(fd,0,SEEK_END)<0)   //将文件偏移量移动到文件尾
        {
            perror("lseek error");
            exit(1);
        }  
        else
            printf("Parent process lseek success!\n"); 
    }
    else
    {
        char *s = "Child process input!\n";
        ssize_t size = strlen(s)*sizeof(char);
        sleep(1);   //等待父进程调节偏移量
        if(write(fd,s,size) != size)   //fd是从父进程中复制来的,指向同一个文件
        {
            perror("write error");   
            exit(1); 
        }
        else
            printf("Child process write success!\n"); 
    }
    printf("current pid : %d\n",getpid());
    sleep(1);   //等待子进程写入完毕
    close(fd);
    return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述
通过上面程序的运行结果可以看到,子进程在父进程写入的文件中追加了部分信息。
在程序中使用多个fork()函数的代码示例如下。

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

int main()
{
    pid_t pid = fork();
    if(pid < 0)
        perror("fork error");
    else
    {
        pid = fork();
        if(pid < 0)
            perror("fork error");
        else
        {
            pid = fork();
            if(pid < 0)
                perror("fork error");
        }
    }
    printf("current pid : %d\n",getpid());
    sleep(1);
    return 0;
}

产生的进程数是以2为底数增长的,代码中使用了几个fork()函数,产生的进程数就是2的几次方。
进程链和进程扇
进程链是指父进程创建子进程,子进程再创建子进程这样的链式结构,每个进程只有向下的一个分支;进程扇指的是一个父进程创建多个子进程,只有父进程有多个分支。
在这里插入图片描述
进程链的代码示例如下。

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

int main(int argc,char* argv[])
{
    int num = 0;
    if(argc < 2)
    {
        fprintf(stderr,"usage : %s number\n",argv[0]);
        exit(1);
    }
    else
    {
        num = atoi(argv[1]);
        if(num < 2)
            num = 2;
    }
    
    pid_t pid;
    for(int i=1;i<num;i++)
    {
        pid = fork();
        if(pid < 0)
        {
            perror("fork error");
            exit(1);
        }
        else if(pid > 0)
            break;    //父进程退出循环
        //子进程就接着执行循环
    }
    printf("pid : %d   ppid : %d \n",getpid(),getppid());
    sleep(1);   //等待所有进程打印完毕
    return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述
关于进程扇的执行结果如下,代码只需要在进程链代码的基础上将父进程退出循环改为子进程退出循环即可。
在这里插入图片描述


进程终止

进程终止分为正常终止、异常终止和进程返回。
正常终止:从main函数返回;调用exit(标准C库函数);调用_exit或_Exit(系统调用);最后一个线程从其启动例程返回;最后一个线程调用pthread_exit。
异常终止:调用abort;接收到一个信号并终止;最后一个线程对取消请求做处理响应。
进程返回:通常程序运行成功返回0,否则返回非0;在shell中可以查看进程的返回值。
每个启动的进程都默认登记了一个标准的终止函数,终止函数在进程终止时释放进程所占用的一些资源,登记的多个终止函数执行顺序是以栈的方式执行,先登记的后执行

#include <stdlib.h>
int atexit(void (*function(void)));   //向内核登记终止函数,成功返回0,出错返回-1

进程终止的简单代码示例如下。

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

//定义进程的终止函数
void term_fun1()
{
    printf("This is the first terminal function!\n");
}

void term_fun2()
{
    printf("This is the second terminal function!\n");
}

void term_fun3()
{
    printf("This is the third terminal function!\n");
}

int main(int argc,char *argv[])
{
    if(argc < 3)
    {
        fprintf(stderr,"usage: %s filename exit or _exit or return\n",argv[0]);
        exit(1);
    }
    //向内核登记终止函数
    atexit(term_fun1);
    atexit(term_fun2);
    atexit(term_fun3);

    FILE *fp = fopen(argv[1],"w");
    fprintf(fp,"hello world!\n");     //写入文件
    if(!strcmp(argv[2],"exit"))
        exit(0);   //标准C库函数
    else if(!strcmp(argv[2],"_exit"))
        _exit(0);  //系统调用
    else if(!strcmp(argv[2],"return"))
        return 0;
    else
       fprintf(stderr,"usage: %s filename exit or _exit or return\n",argv[0]);
    exit(0);
}

程序编译后的执行结果如下图所示。
在这里插入图片描述
可以看到,终止函数确实是按照栈的方式执行的,先登记的后执行,后登记的先执行。
通过上面的执行结果也可以发现return、exit和系统调用_exit/_Exit的区别。

returnexit()_exit() / _Exit()
是否刷新标准I/O缓存
是否自动调用终止函数

僵尸进程

子进程结束但是没有完全释放内存,该进程就称为僵尸进程(zombie)。僵尸进程父进程结束后,该僵尸进程就会被init进程领养,最终被系统回收。
避免僵尸进程的方法:让僵尸进程的父进程来回收,父进程每隔一段时间来查询子进程是否结束并回收,调用wait()或者waitpid(),通知内核释放僵尸进程;采用信号SIGCHLD通知处理,并在信号处理程序中调用wait函数;让僵尸进程成为孤儿进程,由init进程回收。
关于僵尸进程的使用例子如下。

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

int main()
{
    pid_t pid = fork();
    if(pid < 0)
        perror("fork error");
    else if(pid == 0)
    {
        printf("pid : %d  ppid : %d\n",getpid(),getppid());
        return 0;  //子进程结束成为僵尸进程  
        //使用exit(0)结束也可以
    } 
    while(1)
    {
         sleep(5);
    }
    return 0;
}

程序运行后再开一个终端,使用命令查看当前的进程,可以看到,子进程退出后由于没有释放内存,变为了僵尸进程。
在这里插入图片描述
也可以让程序在后台运行,只需要在命令的结尾加上&符号即可,然后使用kill -9命令杀死僵尸进程的父进程,再使用ps命令查看当前的进程信息,僵尸进程就不存在了。
在这里插入图片描述


守护进程和孤儿进程

守护进程(daemon)是生存期长的一种进程,它们常常在系统引导装入时启动,在系统关闭时终止。所有守护进程都以超级用户(用户ID为0)的优先权运行,守护进程没有控制终端,其父进程都是init进程。
父进程结束,子进程就成为孤儿进程(orphan),会由init进程领养。
关于孤儿进程的代码示例如下。

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

int main()
{
    pid_t pid = fork();
    if(pid < 0)
        perror("fork error");
    else if(pid > 0)
    {
        sleep(1);   //父进程先不退出,让子进程打印此时的父进程号
        printf("父进程退出,pid : %d\n",getpid());
        exit(0);
    } 
   else
   {
        int i = 0;
        while(1)
        {
            printf("子进程pid : %d  父进程pid : %d\n",getpid(),getppid());
            i++;
            if(i==2)
                break;
            sleep(2);   //确保父进程退出
        }
   }
   return 0;
}

上面程序编译后运行结果如下图所示。
在这里插入图片描述
可以看到,在父进程还没退出时,子进程打印的父进程号就是创建自己的进程号,在父进程退出后,子进程变为孤儿进程被init进程领养,父进程号也因此变为1。


wait函数

wait函数的原型及其要包含的头文件如下。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);   //status为空时,代表任意状态结束的子进程,不为空则代表指定状态结束的子进程
pid_t waitpid(pid_t pid,int *status,int options); 
//pid是-1,等待任一子进程,功能与wait等效;pid大于0,等待其进程ID与pid相等的子进程;pid等于0,等待其组ID等于调用进程的组ID的任一子进程;pid小于-1,等待其组ID等于pid的绝对值的任一子进程
//成功返回子进程的ID,出错返回-1

检查wait和waitpid函数返回终止状态的宏:
WIFEXITED / WEXITSTATUS(status) 若为正常终止子进程返回的状态,则为真。
WIFSIGNALED / WTERMSIG(status) 若为异常终止子进程返回的状态,则为真(接到一个不能捕捉的信号)。
WIFSTOPPED / WSTOPSIG(status) 若为当前暂停子进程的返回的状态,则为真。
options参数:
WNOHANG 若由pid指定的子进程没有退出则立即返回,则waitpid不阻塞,此时其返回值为0。
WUNTRACED 若某实现支持作业控制,则由pid指定的任一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态。
wait()函数的功能是等待子进程退出并回收,防止僵尸进程的产生;waitpid()函数是wait()函数的非阻塞版本。
wait()函数和waitpid()函数的区别:
在一个子进程终止前,wait使其调用者阻塞;waitpid有一个选择项options,可使调用者不阻塞;waitpid等待一个指定的子进程pid,返回的是等待子进程的状态,而wait等待所有的子进程,其返回任一终止子进程的状态。
使用kill -l命令查看所有信号的代表的意思。
在这里插入图片描述
关于wait函数的使用,其代码示例如下。

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

void output_status(int status)
{
    if(WIFEXITED(status))
        printf("Normal exit : %d\n",WEXITSTATUS(status));
    else if(WIFSIGNALED(status))
        printf("Abnormal exit : %d\n",WTERMSIG(status));
    else if(WIFSTOPPED(status))
        printf("Stopped signal : %d\n",WSTOPSIG(status));
    else
        printf("Unknow signal!\n");
}

int main()
{
    int status;
    pid_t pid;
    if((pid = fork()) < 0)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid == 0)
    {
        printf("pid : %d  ppid : %d\n",getpid(),getppid());
        exit(6);  //子进程正常终止
    }
    wait(&status);  //父进程调用wait函数阻塞,等待子进程结束并回收
    output_status(status);   //输出状态信息

    if((pid = fork()) < 0)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid == 0)
    {
        printf("pid : %d  ppid : %d\n",getpid(),getppid());
        int i=1,j=0;
        int k = i/j;    //除数为0,异常终止
    }
    wait(&status);  //父进程调用wait函数阻塞,等待子进程结束并回收
    output_status(status);   //输出状态信息

    if((pid = fork()) < 0)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid == 0)
    {
        printf("pid : %d  ppid : %d\n",getpid(),getppid());
        pause();     //阻塞等待
        //while(1) sleep(3);  //和上面的pause语句效果一样
    }
    //wait(&status);    //使用wait时,用kill发命令是异常退出,不是停止信号
    do{
        pid = waitpid(pid,&status,WNOHANG | WUNTRACED);   //使用waitpid函数
        if(pid == 0)
            sleep(1);
    }while(pid == 0);
    output_status(status);   //输出状态信息
    return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述
在另一个终端里给当前阻塞的进程发送停止信号,通过返回的status就能打印出相应的错误信息。
一般在有父子进程的程序中,在父进程中加入wait(0),让其等待子进程结束后回收资源。


exec函数

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

#include <unistd.h>
int execl(const char* pathname,const char* arg0, .../*(char*) 0*/);
int execv(const char* pathname,char* const argv[]);
int execle(const char* pathname,const char* arg0,.../*(char*) 0,char *const envp[]*/);
int execve(const char* pathname,char* const argv[],char* const envp[]);
int execlp(const char* pathname,const char* arg0,.../*(char*) 0*/);
int execvp(const char* pathname,char* const argv[]);
//出错返回-1,成功则不返回

以上函数中,除了execve函数是系统调用外,其他函数都是库函数,执行execve函数,后面的代码不执行。
execlp函数和execvp函数中的pathname,相对路径和绝对路径均可使用,其他四个函数中只能使用绝对路径,相对路径一定要在进程环境表对应的PATH中。函数中的argv参数为新程序执行main函数中传递的参数,最后一个元素为NULL,envp是进程的环境表。
在这里插入图片描述
带有字母“I”的函数,表明后面的参数列表是要传递给程序的参数列表,参数列表的第一个参数必须是要执行程序,最后一个参数必须是NULL。
带有字母“p”的函数,第一个参数可以是相对路径或程序名,如果无法立即找到要执行的程序,那么就在环境变量PATH指定的路径中搜索。其他函数的第一个参数则必须是绝对路径名。
带有字母“v”的函数,表明程序的参数列表通过一个字符串数组来传递,这个数组和最后传递给程序的main函数的字符串数组argv完全一样。第一个参数必须是程序名,最后一个参数也必须是NULL。
带有字母“e”的函数,用户可以自己设置程序接收一个设置环境变量的数组。
关于exec函数调用的简单代码示例如下。

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

char *cmd1 = "cat";
char *cmd2 = "/bin/cat";
char *argv1 = "/etc/passwd";
char *argv2 = "/etc/group";

int main(int argc,char *argv[])
{
    pid_t pid;
    if((pid = fork()) < 0)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid == 0)  //子进程运行
    {
        if(execl(cmd2,cmd1,argv1,argv2,NULL) < 0)
        {
            perror("execl error");
            exit(1);
        }
        else
        {
            printf("execl %s success!\n",cmd1);
        }
    } 
    wait(0);  //wait(NULL);  父进程执行
    return 0;
}

上面代码中,如果在execl()函数中使用相对路径,函数无法调用成功,提示文件不存在,第一个参数只能是绝对路径,execl()函数调用成功后,后面的代码也不再执行。


system函数

system函数的原型如下。

#include <stdlib.h>
int system(const char* command);  
//成功返回执行命令的状态,出错返回-1

system函数的简单使用代码如下,打印系统时间。

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

int main()
{
    system("clear");   //清屏
    system("date");   //当前时间
    system("cal");   //本月日历
    return 0;
}

程序编译后的执行结果如下图所示。
在这里插入图片描述
其功能相当于使用bash命令执行。
在这里插入图片描述
system函数的功能就是简化exec函数的使用,其内部构建了一个子进程,并由这个子进程调用exec函数。
关于自定义函数调用exec函数实现system函数的代码示例如下。

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

char *cmd1 = "cal > s1.txt";
char *cmd2 = "cal > s2.txt";
void mysystem(char *cmd)
{
    pid_t pid;
    if((pid = fork()) < 0)
    {
        perror("fork error");
        exit(1);
    }
    else if(pid == 0)
    {
        if(execlp("/bin/bash","/bin/bash","-c",cmd,NULL) < 0)
        {
            perror("execlp error");
            exit(1);
        }
    }
    wait(0);
}

int main()
{
    system(cmd1);   //使用系统函数
    mysystem(cmd2);   //使用自己定义的函数调用exec函数
    return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述


参考视频:
Linux多进程编程详解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/486779.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数智赋能|智慧变电站数字孪生解决方案

当今世界&#xff0c;绿色发展已经成为一个重要趋势&#xff0c;中国、欧盟、北美纷纷发布了通过低碳化、电气化、网络化、智能化全面进行能源结构变革&#xff0c;推进碳达峰、碳中和进程的战略举措。落实绿色发展目标&#xff0c;能源是主战场&#xff0c;电力是主力军&#…

用DataGrip连接hive时报错:User: root is not allowed to impersonate plck5,解决方法

你可以尝试关闭主机校验 修改hive安装目录下conf/hive-site.xml,将hive.server2.enable.doAs设置成false <property><name>hive.server2.enable.doAs</name><value>false</value><description>Setting this property to true will have H…

python 处理png图片无损压缩

代码利用了Pillow库来处理图片的压缩&#xff0c;并使用了 glob 模块来搜索所有的 .png 文件。这个脚本应该能够按照当前的编写来完成预期的工作。 请注意&#xff0c;compress_level9 指定了Pillow保存PNG图片时采用的最大压缩等级。这确保了每张图片都被以可能的最小文件大小…

二叉搜索树(二叉排序树,二叉查找树)(附图详解+代码实现+应用分析)

最近学习了有关搜索二叉树的相关知识&#xff0c;在此特意将该知识进行总结分享&#xff0c;希望对大家有所帮助。 文章目录 一.二叉搜索树1.1二叉搜索树的概念1.2二叉搜索树的操作&#xff08;含思路分析代码实现&#xff09;1.2.1二叉搜索树的查找&#xff08;递归实现看最后…

如何在Ubuntu系统使用Docker搭建MongoDB结合内网穿透实现公网连接

文章目录 前言1. 安装Docker2. 使用Docker拉取MongoDB镜像3. 创建并启动MongoDB容器4. 本地连接测试5. 公网远程访问本地MongoDB容器5.1 内网穿透工具安装5.2 创建远程连接公网地址5.3 使用固定TCP地址远程访问 前言 本文主要介绍如何在Linux Ubuntu系统使用Docker快速部署Mon…

城市排涝与海绵城市规划设计中的水文水动力模拟技术应用

随着计算机的广泛应用和各类模型软件的发展&#xff0c;将排水系统模型作为城市洪灾评价与防治的技术手段已经成为防洪防灾的重要技术途径。本次培训将聚焦于综合利用GIS及CAD等工具高效地进行大规模城市排水系统水力模型的建立&#xff0c;利用SWMM实现排水系统水力模拟。讲解…

如何本地部署Imagewheel并实现无公网IP远程连接打造个人云图床

文章目录 1.前言2. Imagewheel网站搭建2.1. Imagewheel下载和安装2.2. Imagewheel网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar临时数据隧道3.2.Cpolar稳定隧道&#xff08;云端设置&#xff09;3.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 4.公网访问测…

Linux文件系列:磁盘,文件系统,软硬链接

Linux文件系列:磁盘,文件系统,软硬链接 一.磁盘相关知识1.磁盘机械构成2.磁盘物理存储3.磁盘逻辑存储1.LBA地址2.磁盘的分区和分组 二.文件系统和inode1.inode结构体2.文件系统1.Super Block(超级块)2.Group Descriptor Table(块组描述表GDT)3.inode Table4.Data Blocks5.Block…

vue3+threejs新手从零开发卡牌游戏(八):关联卡组和手牌区、添加初始化卡组和初始化手牌逻辑

首先我们优化下之前的代码&#xff0c;先加载游戏资源&#xff0c;然后再初始化场景&#xff0c;由于目前只有一个font字体需要加载&#xff0c;所以我们将之前game/deck/p1.vue中的font相关代码迁移到game/index.vue下&#xff0c;同时使用async和await处理异步加载&#xff0…

基于Scapy国内城市空气质量数据采集系统设计与实现

代码和完整的报告在文章最后 城市空气质量数据采集系统设计与实现 &#x1f3d9;️ 研究背景 &#x1f32c;️ 城市化与环境挑战&#xff1a;随着城市化进程的加快&#xff0c;环境污染问题&#xff0c;尤其是空气质量问题&#xff0c;已成为公众关注的焦点。数据监测的重要性…

Windows安装配置国产达梦数据库、配置Python接口

文章目录 前言1.下载安装达梦数据库2.配置达梦环境变量3.安装Microsoft Visual C 14.04.安装达梦Python接口dmpython5.测试验证 总结 前言 达梦数据库&#xff08;Dameng Database&#xff09;是由武汉达梦数据库股份有限公司开发的一款高性能的关系型数据库管理系统。该数据库…

关于短群签名论文阅读

参考文献为2004年发表的Short Group Signatures 什么群签名&#xff1f; 群签名大致就是由一组用户组成一个群&#xff0c;其中用户对某条消息的签名&#xff0c;改签名不会揭示是哪一个用户签署的&#xff0c;签名只能表明该消息确实是来自该群的签名。对于群还有一个群管理者…

蓝桥杯算法 - DP

上一篇&#xff1a;[[蓝桥杯算法-排序、递归、全排列]] 动态规划&#xff08;dp&#xff09; dp即动态规划&#xff0c;常用于&#xff1a;数学&#xff0c;计算机科学&#xff0c;管理学&#xff0c;经济和生物信息学。 dp在生活中也很常见&#xff0c;如&#xff1a;你今天…

【随笔】oh-my-posh(Windows power shell为例)

Oh My Posh 是一个适用于任何 shell 的自定义提示引擎&#xff0c;能够使用函数或变量调整提示字符串。 文章目录 一、安装oh-my-posh二、安装Nerd 字体三、oh-my-posh 初始化四、更换主题 一、安装oh-my-posh GitHub repo&#xff1a;https://github.com/JanDeDobbeleer/oh-m…

情感视频素材怎么来的?(情感语录的视频素材在哪里找)

很多小伙伴觉得情感类型的短视频账号用户多&#xff0c;都想要进入分一杯羹&#xff0c;那么这些创作素材去哪里找呢&#xff0c;下面分享几个非常使用的找情感短视频素材的办法。 1&#xff0c;蛙学网 说到情感视频素材的短视频&#xff0c;作为一个专业的短视频素材网站&am…

2024年云服务器ECS价格表出炉——腾讯云

腾讯云服务器多少钱一年&#xff1f;61元一年起。2024年最新腾讯云服务器优惠价格表&#xff0c;腾讯云轻量2核2G3M服务器61元一年、2核2G4M服务器99元一年可买三年、2核4G5M服务器165元一年、3年756元、轻量4核8M12M服务器646元15个月、4核16G10M配置32元1个月、312元一年、8核…

nodeJs中实现连表查询

nodeJs中实现连表查询 router.post(/getOrder, async function(req, res, next) {let userId req.body.phone;let sql select * from orders where userId?;let orders await new Promise((resolve, reject) > {connection.query(sql, [userId], function(error, resul…

一分钟在Solana链创建代币教程

只需要 1 分钟就可以创建自己的SOLANA代币 1、连接Solana钱包2、填写代币信息创建3、创建成功 Solana 是一个基于区块链技术的高性能、去中心化的智能合约平台&#xff0c;旨在为开发者提供高度可扩展和低成本的区块链基础设施。通过其创新的共识机制和高吞吐量的网络架构&…

注册中国商标的大致流程

在当今竞争激烈的商业环境中&#xff0c;商标作为企业形象和品牌标识的重要载体&#xff0c;其保护和推广至关重要。注册中国商标是拓展中国市场的关键步骤 注册中国商标需要以下基本资料&#xff1a; 商标图样&#xff1a;须清晰、完整地展示商标图案和文字内容&#xff1b;商…

MQ消息队列从入门到精通速成

文章目录 1.初识MQ1.1.同步和异步通讯1.1.1.同步通讯1.1.2.异步通讯 1.2.技术对比&#xff1a; 2.快速入门2.1.安装RabbitMQ2.2.RabbitMQ消息模型2.3.导入Demo工程2.4.入门案例2.4.1.publisher实现2.4.2.consumer实现 2.5.总结 3.SpringAMQP3.1.Basic Queue 简单队列模型3.1.1.…