Unix 进程 API 介绍

简介: Unix 进程 API 介绍

本文,主要介绍进程创建的几个接口,带领大家了解进程创建与控制过程。


fork 系统调用


如下,为一个fork调用基本示例:


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv)
{
    int pid = -1;
    pid = getpid();
    printf("hello world (pid:%d)\n", pid);
    int ret = -1;
    ret = fork();
 if (ret < 0) {
      fprintf(stderr, "fork failed\n");
      exit(1);
 } else if (ret == 0) { /* child: new process */
      pid = getpid();
      printf("hello, I am child (pid:%d)\n", pid);
 } else {               /* parent process */
      pid = getpid();
      printf("hello, I am parent of %d (pid:%d)\n", ret, pid);
 }
  return 0;
}


image.png


当这个程序运行时,首先输出hello world信息,以及自己的进程描述符(PID = 21281)。


紧接着进程掉调用了 fork() 系统调用,这是操作系统提供的创建进程的方法。新创建的进程几乎与调用进程完全一样,对于操作系统来说,这是看起来两个完全一样的程序在运行,并且都从fork系统调用返回。


新创建的进程称为子进程(child)、原来的进程称为父进程(parent),子进程不会从main函数开始执行,而是直接从fork系统调用返回,就好像它自己调用了fork()。父进程获得返回值是创建新进程的PID,而子进程获得返回值是0.


子进程并不是完全拷贝父进程。虽然它拥有自己的地址空间、寄存器、程序寄存器等,但是它从fork()返回值是不同的。


注意:子进程被创建后,它们的输出先后是随机的,这涉及到CPU调度,决定了哪个时刻该运行哪个程序。


wait 系统调用


fork()系统调用,只是创建了一个子进程,其他什么也没做。有时候,我们需要父进程等待子进程执行完毕。这项任务由wait()系统调用(或者更完整的兄弟接口waitpid())完成。


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv)
{
    int pid = -1;
    pid = getpid();
    printf("hello world (pid:%d)\n", pid);
    int ret = -1;
    ret = fork();
 if (ret < 0) {
      fprintf(stderr, "fork failed\n");
      exit(1);
 } else if (ret == 0) { /* child: new process */
      pid = getpid();
      printf("hello, I am child (pid:%d)\n", pid);
 } else {               /* parent process */
      int wc = wait(NULL);
      pid = getpid();
      printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", ret,wc,  pid);
 }
  return 0;
}


image.png


在这个例子中,父进程调用wait(),延迟自己的执行,直到子进程执行完毕。当子进程完结束后,wait()才返回父进程。


exec 系统调用


exec() 系统调用,它也是创建进程API的一个重要组成部分。这个系统调用可以让子进程执行与父进程不同的程序。(exec有几种变体,execl、execle、execlp、execv和execvp)


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char *argv)
{
    int pid = -1; 
    pid = getpid();
    printf("hello world (pid:%d)\n", pid);
    int ret = -1; 
    ret = fork();
    if (ret < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (ret == 0) { /*child: new process*/
        pid = getpid();
        printf("hello, I am child (pid:%d)\n", pid);                                                                                                                                                        
        char *myargs[3];
        myargs[0] = strdup("wc");  /* program: wc (word count) */
        myargs[1] = strdup("exec.c");/* argumnt: file count*/
        myargs[2] = NULL;             /* marks end of array*/
        execvp(myargs[0], myargs);  /* runs word count */
        printf("this is shouldn't print out");
    } else {               /* parent process */
        int wc = wait(NULL);
        pid = getpid();
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", ret,wc, pid);
    }
    return 0;
}


image.png            

                 

在这个例子中,子进程调用execvp()来运行字符计数程序wc。实际上,它针对源码文件 exec.c 运行wc,从而告诉我们该文件有多少行,多少单词,以及多少字节。


exec调用,对于给定可执行程序的名称(如wc)以及需要的参数后,就会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化。然后操作系统就执行该程序,将参数通过argv传递给进程。


注:想象一下,可执行程序的加载,是不是很像?


因此,它并没有创建新的进程,而是直接将当前运行的程序(以前的a.out)替换为不同的运行程序(wc)。


子进程执行exec()后,几乎就像exec.c 从未运行过一样。对exec的成功调用永远也不会返回。


为什么要这样设计API?


事实证明,这种分离fork()以及exec()的做法在构建UNIX shell的时候非常有用,因为这给了 shell 在fork 之后 exec 之前运行代码的机会,这些代码可以在运行新程序前改变环境。


shell 也是一个用户程序。它首先是一个显示提示符(prompt),然后等待用户输入。你可以向它输入命令(一个可执行程序的名称及参数),大多数情况下,shell 可以在文件系统中找到这个程序,调用fork()创建新进程,并调用exec()的某个变体来执行这个可执行程序,调用wait()等待该命令的完成。子进程执行结束后,shell 从wait()返回并再次输出提示符,等待用户输入下一个命令。


fork() 和 exec()的分离,让shell 可以方便地实现很多有用的功能。比如:


/* 将输出结果重定向到 newfile.txt*/
wc exec.c > newfile.txt


  • 创建子进程。


  • shell 在调用 exec() 之前,关闭了标准输出,打开了文件 newfile.txt


  • 然后将结果写到 newfile.txt.


如下展示了重定向的基本原理:


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <fcntl.h>
int main(int argc, char *argv)
{
    int pid = -1;
    pid = getpid();
    printf("hello world (pid:%d)\n", pid);
    int ret = -1; 
    ret = fork();
    if (ret < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (ret == 0) { /*child: new process*/
        /*pid = getpid();*/
        /*printf("hello, I am child (pid:%d)\n", pid);*/
        close(STDOUT_FILENO);
        open("./redirect.output", O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU);
        // now exec wc
        char *myargs[3];
        myargs[0] = strdup("wc");  /* program: wc (word count) */
        myargs[1] = strdup("redirect.c");                   myargs[2] = NULL;
        execvp(myargs[0], myargs);
      } else {               /* parent process */
          int wc = wait(NULL);
          //pid = getpid();
          // printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", ret,wc, pid);
      }
      return 0;
}



UNIX 系统从0 开始寻找可以使用的文件描述符。在这个例子中,STDOUT_FILENO将成为第一个可用的文件描述符。因此在OPEN()被调用时,得到赋值,然后子进程向标准输出文件的写入(例如printf()这样的函数),都会被透明转向新打开的文件,而不少屏幕。


总结


本文只是从较高层面简单介绍了进程API,关于这些系统调用的细节,需要深入学习可了解。另外,除了fork()、exec()、wait()之外,在UNIX中还有其他许多与进程交互的方式。

相关文章
|
Unix 程序员 Linux
【OSTEP】动态内存开辟 | 内存API常见错误 | UNIX: brk/sbrk 系统调用 | mmap创建匿名映射区域 | mmap创建以文件为基础的映射区域
【OSTEP】动态内存开辟 | 内存API常见错误 | UNIX: brk/sbrk 系统调用 | mmap创建匿名映射区域 | mmap创建以文件为基础的映射区域
276 0
|
5月前
|
开发框架 Unix Linux
LangChain 构建问题之在Unix/Linux系统上设置OpenAI API密钥如何解决
LangChain 构建问题之在Unix/Linux系统上设置OpenAI API密钥如何解决
72 0
|
存储 SQL Shell
【OSTEP】Abstraction Process | 进程 | 虚拟化 | 进程API
【OSTEP】Abstraction Process | 进程 | 虚拟化 | 进程API
61 0
|
8月前
|
消息中间件 监控 安全
探究如何在Linux系统中修改进程资源限制:四种方法调整进程限制,让你的系统高效运行(包含应用层getrlimit和setrlimit API)
探究如何在Linux系统中修改进程资源限制:四种方法调整进程限制,让你的系统高效运行(包含应用层getrlimit和setrlimit API)
1243 0
|
8月前
|
Java API 调度
Java多线程基础(线程与进程的区别,线程的创建方式及常用api,线程的状态)
Java多线程基础(线程与进程的区别,线程的创建方式及常用api,线程的状态)
82 0
Java多线程基础(线程与进程的区别,线程的创建方式及常用api,线程的状态)
|
安全 Unix Shell
Unix进程相关用户ID、用户组ID详解
我们在使用类UNIX系统时,经常会涉及到各种ID,比如,文件属性相关的用户ID、组ID,进程运行时相关的6个ID:实际ID、实际组ID、有效ID、有效组ID、保存的用户设置ID、保存的设置组ID。
482 0
|
Java API 调度
Java多线程基础(线程与进程的区别,线程的创建方式及常用api,线程的状态)
每一个线程都是一个执行流,都按照自己的顺序执行自己的代码,多个线程之间“同时”(并发并行)的执行多份代码。Java中的线程是以轻量级进程来实现的。
Java多线程基础(线程与进程的区别,线程的创建方式及常用api,线程的状态)
|
存储 Unix C语言
《UNIX环境高级编程》第七章进程环境
7.2 main函数 1.C程序总是从main函数开始执行的,原型:int main(int argc,char *argv[]);argc是命令行参数的个数argc是指向参数的各个指针所构成的数组2.内核执行C程序时,在调用main前先调用一个特殊的启动例程。
873 0
|
6月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
6月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
213 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)