Linux系统编程——Linux进程控制

Linux系统编程——Linux进程控制

1. 进程创建

我们知道,进程是描述程序的执行过程,那么我们可以在程序的内部创建其他进程,用来调用其他程序执行过程。Linux中的fork函数,其作用就是创建一个新的进程,而这个进程被称为子进程

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

分析一下该函数,它有多个返回值,如果函数正常创建了子进程,子进程中返回0,父进程返回子进程的ID,如果出错返回-1。

为什么一个函数可以根据不同的进程所返回不同的结果呢?我们的进程在调用了fork()函数之后,其内部做了以下事情。

  • 分配了一个新的内存块和内核数据结构给子进程。
  • 将父进程部分数据结构内容拷贝给子进程
  • 添加子进程到系统进程列表中
  • fork返回,开始调度器调度

在这里插入图片描述

当一个进程在调用fork的时候,其实就会赋值当前进程的二进制代码给子进程,两个进程就开始各自执行之后的代码。

int main( void )
{
    pid_t pid;
    
    printf("Before: pid is %d\n", getpid()); 
    
    // 如果fork失败
    if ( (pid = fork()) == -1 )
    {
    	perror("fork()");
    	exit(1);
    }
   
    printf("After:pid is %d, fork return %d\n", getpid(), pid); 
    sleep(1);
    return 0;
}

执行结果:

Before: pid is 43676
After:pid is 43676 fork return 43677
After:pid is 43677, fork return 0

我们看到,这里有三行输出,一行before,两行after。父进程先打印了before的消息,然后另外父进程和子进程都有打印after的消息。但是,为什么子进程没有执行fork函数之前的代码呢?是因为在fork之后,父子两个轮流执行之后的代码,之前执行的代码不会在执行了。而after的消息输出,是父进程和子进程都有可能先输出,因为这要根据调度器来决定。你可以尝试输出几次,会得到不一样的结果。

1.1 写时拷贝

通常,父子代码共享的时候,父进程不写入的时候,数据也会共享的,这是因为,如果我们只读数据的话,两个进程共享一份相同的数据是不会有任何影响的。但当其中一方需要修改对应的数据时,这时便会采用写时拷贝的方式在对应的物理地址上复制一份副本

在这里插入图片描述

2. 进程终止

进程终止有三种情况,分别如下:

  1. 代码运行完毕,结果正确。
  2. 代码运行完毕,结果不正确。
  3. 代码异常终止。

正常终止

  • 从main函数返回
  • 调用exit
  • 调用_exit

异常退出

  • ctrl+c ,信号终止

main函数其实是通过return来进程退出,return n就是执行exit(n),而返回0表示进程正常退出,非0表示异常退出。

下面介绍exit_exit

#include <unistd.h>
void _exit(int status);

void exit(int status);

status是状态码的意思,它定义了进程的终止状态,父进程可以通过wait来获取子进程的返回状态吗。而在linux下,如果执行完一个进程,想获得其状态码,可以在终端获取$?

其实,exit本质上是调用了_exit。并且在调用之前,还做了其他工作。

  1. 执行用户通过atexiton_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit

案例:

int main()
{
    printf("hello");
    exit(0);
}

3. 进程等待

为什么需要进程等待呢?考虑一种情况,如果子进程退出,父进程不管子进程,就可能会造成僵尸进程的问题,从而造成内存泄露。

并且,子进程一旦称为僵尸进程,也没办法杀死,因为谁也没办法杀死一个已经死去的进程。

最后,父进程派给子进程一个任务,我们并不知道子进程完成这个任务的情况。运行结果是正确还是错误,有没有正常退出等待。

进程等待的方法

·wait

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status);
  • 返回值是返回被等待的进程pid,失败返回-1。
  • 参数是获取子进程退出状态,如果不关心可以传入NULL。

·waitpid

pid_t waitpid(pid_t pid,int *status,int options);
  • 返回值:
    • 如果正常返回的时候,waitpid就返回子进程的id。
    • 如果设置了选项WNOHANG,而调用的时候waitpid发现没有可回收的子进程,返回0。
    • 如果调用中出错,返回-1。并设置对应的errno码。
  • 参数:
    • pid:
      • pid = -1,等待任意一个子进程。与wait一致。
      • pid > 0, 等待指定的pid子进程。
    • status:
      • WIFEXITED(status),若为正常终止子进程返回的状态,则为真。
      • WEXITSTATUS(status),若WIFEXITED非零,提取子进程退出码。
    • options:
      • WNOHANG:若pid指定的子进程没有结束,立即返回0。若正常结束,返回子进程的pid。

获取子进程的status

  • wait和waitpid,都有一个status参数,该参数是由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • status本质上是一个位图,不能当成整形看待。(只关心低16位)

在这里插入图片描述

测试代码:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(void)
{
    pid_t pid;
    // 创建子进程
    if ((pid = fork()) == -1)
        perror("fork"), exit(1);
    if (pid == 0)
    {
        // 子进程睡眠20秒后退出,返回状态码10
        sleep(20);
        exit(10);
    }
    else
    {
        int st;
        int ret = wait(&st);
        // 如果正常退出,说明状态码低7位是全0
        if (ret > 0 && (st & 0X7F) == 0)
        { // 正常退出
            // 正常退出的status为101000000000 >> 8 = 1010 && 0xff = 1010 为 10
            printf("child exit code:%d\n", (st >> 8) & 0XFF);
        }
        else if (ret > 0)
        { // 异常退出
            // 异常退出,获取对应信号
            printf("sig code : %d\n", st & 0X7F);
        }
    }
}
3.1 进程阻塞等待和非阻塞等待

阻塞等待

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

int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        printf("%s fork error\n", __FUNCTION__); // __FUNCTION__返回所在的函数
        return 1;
    }
    else if (pid == 0)
    { 
        // child
        printf("child is run, pid is : %d\n", getpid());
        sleep(5);
        exit(257);
    }
    else
    {
        int status = 0;
        pid_t ret = waitpid(-1, &status, 0); //阻塞式等待,等待5S
        printf("this is test for wait\n");
        if (WIFEXITED(status) && ret == pid)
        {
            printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
        }
        else
        {
            printf("wait child failed, return.\n");
            return 1;
        }
    }
    return 0;
}

非阻塞等待

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        printf("%s fork error\n", __FUNCTION__);
        return 1;
    }
    else if (pid == 0)
    { // child
        printf("child is run, pid is : %d\n", getpid());
        sleep(5);
        exit(1);
    }
    else
    {
        int status = 0;
        pid_t ret = 0;
        do
        {
            ret = waitpid(-1, &status, WNOHANG); //非阻塞式等待
            if (ret == 0)
            {
                printf("child is running\n");
            }
            sleep(1);
        } while (ret == 0);
        if (WIFEXITED(status) && ret == pid)
        {
            printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
        }
        else
        {
            printf("wait child failed, return.\n");
            return 1;
        }
    }
    return 0;
}

4. 进程程序替换

fork创建子进程后执行的是和父进程相同的程序(但可能执行不同的代码分支),子进程往往会调用exec相关的函数来执行另一个程序。当进程调用一种exec函数的时候,该进程的用户空间代码和数据完全被新进程替换,从新进程的启动开始执行。

替换函数

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

这些函数,其实只做一件事情,就是进程程序替换。可以根据命名来理解,path为传入的路径。file传入的是文件,会从环境变量去搜索,无需全路径。而envp则是环境变量。最后arg,...argv[]的区别在于是一个一个参数传入,还是传入一个数组。

测试代码:
#include <unistd.h>
int main()
{
    char *const argv[] = {"ps", "-ef", NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    execl("/bin/ps", "ps", "-ef", NULL);
    // 带p的,可以使用环境变量PATH,无需写全路径
    execlp("ps", "ps", "-ef", NULL);
    // 带e的,需要自己组装环境变量
    execle("ps", "ps", "-ef", NULL, envp);
    execv("/bin/ps", argv);
    // 带p的,可以使用环境变量PATH,无需写全路径
    execvp("ps", argv);
    // 带e的,需要自己组装环境变量
    execve("/bin/ps", argv, envp);
    exit(0);
}

在这里插入图片描述

5. 简易shell程序

我们可以使用程序替换来做一个简易的shell。

比如实现ls命令来查询当前列表,使用ps获取进程状态等等。

**原理:**父进程获取命令,然后创建子进程,子进程采用程序替换来调用这些命令对应的程序。直到执行完毕后退出子进程,父进程回收子进程的资源。

在这里插入图片描述
代码如下:

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

#define NUM 1024
#define SIZE 32 
#define SEP " "

// 保存切割后的命令字符串
char *g_argv[SIZE];
// 保存完整的命令行字符串
char cmd_line[NUM];

char g_myval[64];

int main()
{
    // 0.命令行解释器,一定是一个常驻内存的进程,不退出
    while(1)
    {
        // 1. 打印提示信息
        // [chakming@localhost test_09_22]$
        // char buf[NUM] = {0};
        // getcwd(buf,sizeof(buf));
        printf("[chakming@localhost myshell]$ ");
        fflush(stdout); 

        // 2. 获得用户的键盘输入[指令和选项]
        memset(cmd_line,'\0',sizeof(cmd_line));
        // fgets
        if(fgets(cmd_line,sizeof cmd_line,stdin) == NULL)
            continue;
        // 去掉'\n'
        cmd_line[strlen(cmd_line)-1] = '\0';
        // printf("echo : %s\n",cmd_line);

        // 3.切割输入的指令 "ls -l -a" -> "ls" "-l" "-a"
        g_argv[0] = strtok(cmd_line,SEP);
        int index = 1;
        if(strcmp(g_argv[0],"ls") == 0) // 不能让子进程执行
        {
            g_argv[index++] = "--color=auto"; // 改变颜色
        }
        // 如果输入ll 相当于输入ls -l --color=auto
        if(strcmp(g_argv[0],"ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[index++] = "-l"; 
            g_argv[index++] = "--color=auto"; // 改变颜色
        }
        // 第二次传入,如果是解析原始字符串,传入NULL
        while(g_argv[index++] = strtok(NULL,SEP));
        if(strcmp(g_argv[0],"export") == 0 && g_argv[1] != NULL)
        {
            strcpy(g_myval,g_argv[1]);
            int ret = putenv(g_myval);
            if(ret == 0) printf("%s export success\n",g_argv[1]);
            continue;
        }
        // for(index = 0;g_argv[index];index++)
        //     printf("g_argv[%d]:%s\n",index,g_argv[index]);

        // 4.TODO,内置命令,让父进程自己执行的命令,叫做内置命令
        // 本质上就是shell函数调用
        if(strcmp(g_argv[0],"cd") == 0) // 不能让子进程执行
        {
            if(g_argv[1] != NULL)
                chdir(g_argv[1]);
            continue;
        }

        // 5.fork()
        pid_t id = fork();
        if(id == 0)
        {
            execvp(g_argv[0],g_argv);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id,&status,0);
        if(ret > 0) printf("exit code: %d\n",WEXITSTATUS(status));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值