Ⅰ. 进程控制块PCB

task struct结构体:

  1. 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
  2. 进程的状态,有运行、挂起、停止、僵尸等状态。
  3. 进程切换时需要保存和恢复的一些CPU寄存器。
  4. 描述虚拟地址空间的信息。
  5. 描述控制终端的信息。
  6. 当前工作目录(Current Working Directory)
  7. umask掩码。
  8. 文件描述符表,包含很多指向file结构体的针。
  9. 和信号相关的信息。
  10. 用户id和组id
  11. 控制终端、Session和进程组。
  12. 进程可以使用的资源上限(Resource Limit)

Ⅱ. 进程控制fork

fork的作用是根据一个现有的进程复制出一个新进程,原来的进程称为父进程(Parent Process),新进程称为子进程(Child Process)。系统中同时运行着很多进程,这些进程都是从最初只有一个进程(PID为1)开始一个一个复制出来的。可以用命令pstree查看进程的家族图谱。

在Shell下输入命令可以运行一个程序,是因为Shell进程在读取用户输入的命令之后会调用fork复制出一个新的Shell进程。

Linux系统编程笔记(6)——进程体系与进程管理(1)-萤火
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
  1. 返回值:调用失败返回-1并设置errno,调用成功的话:
    • 在父进程中返回子进程的PID
    • 在子进程中返回0
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    pid_t pid;
    char* message;
    int n;
    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(-1);
    }

    if (pid == 0) {
        // 子进程执行代码块
        message = "This is the child process\n";
        n = 6;
    } else {
        // 父进程执行代码块
        message = "This is the parent process\n";
        n = 3;
    }
    // 下面的是父、子进程都会执行的代码块
    while (n > 0) {
        printf("%s", message);
        n--;
        sleep(1);
    }

    return 0;
}
Linux系统编程笔记(6)——进程体系与进程管理(1)-萤火
#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

fork在子进程中返回0,子进程可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法。

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

int main() {
    pid_t pid;
    int n;
    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(-1);
    }

    if (pid == 0) {
        // 子进程执行代码块
        n = 6;
        while (n) {
            // 当父进程结束之后,子进程会成为孤儿进程,被其它进程接管,此时ppid==1
            printf("This is the child process. pid=%d, ppid=%d\n", getpid(), getppid());
            n--;
            sleep(1);
        }

    } else {
        // 父进程执行代码块
        n = 3;
        while (n) {
            printf("This is the parent process. pid=%d, ppid=%d\n", getpid(), getppid());
            n--;
            sleep(1);
        }
    }
    // 下面的是父、子进程都会执行的代码块

    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

/**
 * @brief 创建10个子进程,并打印它们的pid和ppid
 * 
 * @return int 
 */
int main() {
    pid_t pid;
    int n = 10;
    while (n) {
        pid = fork();
        if (pid < 0) {
            perror("fork failed");
            exit(-1);
        }
        if (pid == 0) {
            // 子进程
            printf("This is the child process. pid=%d, ppid=%d\n", getpid(), getppid());
            break;
        }
        // 父进程会执行下面的代码,子进程因为break了所以不会执行
        n--;
    }
    sleep(1);  // 等一下,免得父进程死得比子进程早,让子进程成为孤儿进程
    return 0;
}

Ⅲ. exec函数族

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。

#include <unistd.h>

extern char **environ;
int execl(const char *path, const char *arg, ...
                       /* (char  *) NULL */);
int execlp(const char *file, const char *arg, ...
                       /* (char  *) NULL */);
int execle(const char *path, const char *arg, ...
                       /*, (char *) NULL, 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[]);

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。

1.环境变量表environ

extern char **environ;
exec系统调用执行新程序时会把命令行参数和环境变量表传递给main函数。和命令行参数argv类似,环境变量表也是一个字符串数组
name=value

可以用getenv函数,查询对应name的value

#include <stdlib.h>

char *getenv(const char *name);

可以用setenv函数,将环境变量name的值设置为value

#include <stdlib.h>

int setenv(const char *name, const char *value, int rewrite);
void unsetenv(const char *name);
#include <stdio.h>
#include <unistd.h>

/**
 * @brief 打印环境变量
 * 
 * @return int 
 */
int main() {
    extern char **environ;

    // 读取environ直到遇到NULL(哨兵)
    // for (int i = 0; environ[i] != NULL; i++) {
    //     printf("%s\n", environ[i]);
    // }
    while (*environ) {
        printf("%s\n", *environ);
        environ++;
    }

    return 0;
}
/**
 * @brief 获取和设置PATH环境变量
 * 
 * @return int 
 */
int main() {
    printf("PATH = [%s]\n", getenv("PATH"));
    setenv("PATH", "hello", 1);  // 改的只是当前进程的环境变量,因为这个进程是由bash fork出来的子进程,修改环境变量不影响父进程
    printf("PATH = [%s]\n", getenv("PATH"));

    return 0;
}

2.exec函数族

  • 带有字母l (表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,最后一个可变参数应该是NULL,起sentinel的作用。
  • 对于带有字母v (表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,像main函数的argv参数或者环境变量表environ一样。
  • 不带字母p (表示path)的exec函数第一个参数必须是程序的相对路径或绝对路径,例如"/bin/ls""./a. out"
    对于带字母p的函数:如果参数中包含/,则将其视为路径名。否则视为不带路径的程序名,在PATH环境变量的日录列表中搜索这个程序。
  • 对于以e (表示environment)结尾的exec函数,可以把一份新的环境变量表其他exec函数仍使用当前的环境变量表执行新程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/**
 * @brief 实现exec族函数调用命令
 * 
 * @return int 
 */
int main() {
    // execlp("ls", "ls", "-a", "-l", NULL);  // arg[0]其实写什么都一样,因为ls解析参数是从arg[1]开始,NULL起到哨兵作用
    char *arg[] = {"ls", "-a", "-l", NULL};
    execvp("ls", arg);
    perror("exec");
    exit(-1);
}
#include <ctype.h>
#include <stdio.h>
/**
 * @brief 将输入字符转为大写形式
 * 
 * @return int 
 */
int main() {
    int ch;
    while (~(ch = getchar())) {
        putchar(toupper(ch));
    }

    return 0;
}

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

/**
 * @brief 实现流的重定向
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char* argv[]) {
    if (argc < 3) {
        printf("usage:cmd + input_filename + output_filename\n");
        exit(-1);
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("read");
        exit(-2);
    }
    dup2(fd, 0); // 标准输入重定向
    close(fd);

    fd = open(argv[2], O_WRONLY | O_CREAT, 0644);
    if (fd < 0) {
        perror("write");
        exit(-3);
    }
    dup2(fd, 1); // 标准输出重定向
    close(fd);
    execl("./upper", "./upper", NULL); // 调用./upper命令
    perror("exec");
    exit(-4);
}