Linux C 进程

进程

UNIX编程手册第6 7章完结 24 25 26 27 28
未完待续,可能等到期末考试结束吧

基础知识

进程号是唯一标识进程的正数,数据类型是pid_t,程序和进程号没有固定关系,init进程的pid_t总是1

#include <unistd.h>

pid_t getpid(void);  // 返回该进程的进程号
pid_t getppid(void);  // 返回该进程父进程的进程号

Linux限制进程号不大于32767(可以使用cat /proc/sys/kernel/pid_max来查询此Linux内核支持的最大进程号),每一次创建新进程,内核按顺序分配进程号,当进程号到达最大时,内核重置进程号分配器,重置为300,因为300内有大量Linux守护进程和系统进程。

内存分布

进程分配进入内存时分成很多段,可以使用size命令查看二进制文件的文本段,初始化数据段,非初始化数据段。

  • 文本段

    • 程序的机器语言,只读,可以被多个进程执行
  • 初始化数据段

    • 显式初始化的全局变量和静态变量
  • 未初始化数据段(BBS段)

    • 未显式初始化的全局变量和静态变量

    • 初始化和未初始化数据段分开存储,这样程序就不需要存储未初始化的数据

      因为这些数据可以等到程序加载时再处理,所以不必占用磁盘空间

    • 运行时的数据

命令行参数

int main(int argc, char **argv)

命令行参数由Shell解析,argv[0]是程序本身,argv[argc]NULL

环境列表

获得环境

全局变量char **environ存储了环境列表,其组织形式类似argv

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

extern char **environ;

int main(int argc, char *argv[])
{
    char **ep;

    for (ep = environ; *ep != NULL; ep++)
        puts(*ep);

    exit(EXIT_SUCCESS);
}

可以用main函数第三个参数来访问环境列表,因此上面的代码可以这样写

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

int main(int argc, char **argv, char **envp)
{
    char **ep;

    for (ep = envp; *ep != NULL; ep++)
        puts(*ep);

    exit(EXIT_SUCCESS);
}

使用getenv函数检索环境变量中的值,若不存在该环境变量则返回NULL。

#include <stdlib.h>

char *getenv(const char *name)

使用该函数的警示:由于函数返回字符串的指针,所以调用者有能力修改他,这是危险的,但是我的设备上没有影响哎

/**
 * 危险的程序
 * export say_hello="hello world"
 * echo ${say_hello}
 **/

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

int main(int argc, char **argv)
{
    char *ep = NULL;
    ep = getenv("say_hello");

    if (ep != NULL)
    {
        printf("i change the first char with #\n");
        ep[0] = '#';
        printf("%s\n", getenv("say_hello"));
    }
    else
    {
        printf("cound not find\n");
    }
    exit(EXIT_SUCCESS);
}

/**
 * echo ${say_hello} # 仍然输出正确
 **/

修改环境

仅仅是临时性的(对于该进程和其子进程有效)修改,如要永久修改,需要对文件操作。

int putenv(char *string);

设置环境变量的指针指向这个string,所以string参数绝对不能是栈中的变量

非本地跳转

goto语句只能实现函数内的跳转,不支持函数间的跳转,尤其是其他函数希望“调用”main函数,不能通过函数的返回来实现(因为返回main后将继续顺序执行而不是从头执行,所以非本地跳转解决的是函数间任意位置的跳转问题(但是这就像goto一样,滥用会导致程序逻辑混乱)。

#include <setjmp.h>

int setjmp(jmp_buf env);

void longjmp(jmp_buf env, int val);

程序逻辑:首先设置setjump函数规定跳转的落地位置和环境,程序自然执行setjmp,此时设置进程环境到env(其中也设置了程序计数寄存器栈指针寄存器,用于longjmp调用时信息)并返回0,程序调用某一函数,函数中longjmp根据传入的env返回到对应的setjmp中(基于程序计数寄存器和栈指针寄存器),同时设置val用于表明这次跳转的来源。

内存分配

在堆上分配内存malloc()

void *malloc(size_t size);

malloc函数返回的内存块支持大多数硬件架构的对齐方式,通常基于8对齐或者16对齐的,调用malloc(0)在linux下的行为是创建可以用free释放的内存。调用时若调用成功则增加program break的位置,否则输出时会返回NULL并设置errno。

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

int main(int argc, char **argv)
{
    int *a = NULL;
    a = (int*)malloc(0);
    if(a == NULL)
    {
        printf("still null\n");
    }
    else
    {
        printf("have a space\n");  // 在linux下输出这个
        free(a);
    }
    return 0;
}

释放内存free()

void free(int *ptr);

free函数仅仅释放空间,并不对指针变量设置为NULL,free不改变program break的位置,只是将这个空闲空间加到内存空闲内存列表中,供后续malloc使用。free(NULL)不会产生错误,所以这样的代码没有问题(把NULL加到空闲列表不会有错)。但是对一个已经free却没有设置为NULL的指针进行二次free时,free在尝试将这个内存块放入空闲内存块表中时会出现错误,导致未知问题。

这是一个测试的代码

/**
 * 验证free空指针没有问题
 * 验证free的调用时先将这个内存定位空
 * 再将这个空内存空间放在这个内存表中
 * */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    int *a = NULL;
    a = (int*)malloc(sizeof(int) * 10);
    free(a);
    if(a == NULL)
    {
        printf("a is null\n");
        free(a);
        printf("free a null is safe\n");
    }
    else
    {
        a = NULL;
        free(a);
        printf("a is null after set and free a null is safe\n");
    }
    return 0;
}

其他分配内存函数

void *calloc(size_t __nmemb, size_t __size);
void *realloc(void *ptr, size_t size);

后者是对调整内存块的大小,通常是增加,ptr是需要调整空间的指针,size是大小,如果成功,返回调整后的指针(说明这里的指针不止指向内存块的地址,可能还包含内存块大小的信息),否则返回NULL,realloc(prt, 0)等价于free,realloc(NULL, size)等价于malloc。

newptr = realloc(prt, newsize);

if(nptr == NULL)
{
    /* 处理错误 */
}
else
{
    ptr = newptr;
}

内存对齐函数memalign()

分配内存时,起始地址要求的对齐

void *memalign(size_t alignment, size_t size);

alignment指明对齐要求,size指明大小,但alignment等于8时等价于malloc,这个函数用于设备级别,一个havefun代码

/**
 * 学习使用memalign函数,探索对齐要求的用法
 * 这里按照256对齐,所以地址显示最后两位是00
 * */

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <alloca.h>

int main(int argc, char **argv)
{
    int i = 0;
    char *a = NULL, *b = NULL, *c = NULL;
    a = (char*)malloc(21);
    b = (char*)memalign(256, 20);
    c = (char*)malloc(20);
    for(i = 0; i < 5; i++)
        printf("%lx\n", (long)&a[i]);
    printf("\n");
    for(i = 0; i < 5; i++)
        printf("%lx\n", (long)&b[i]);
    printf("\n");
    for(i = 0; i < 5; i++)
        printf("%lx\n", (long)&c[i]);
    return 0;
}
dwr@ali-s:~/Workspace/C/UNIX manual/7$ ./memalign 564bcd58a2a0
564bcd58a2a1
564bcd58a2a2
564bcd58a2a3
564bcd58a2a4

564bcd58a300
564bcd58a301
564bcd58a302
564bcd58a303
564bcd58a304

564bcd58a410
564bcd58a411
564bcd58a412
564bcd58a413
564bcd58a414
int posix_memalign(void **memptr, size_t alignment, size_t size);
void *alloca(size_t size);

在栈帧上分配内存,和malloc从堆分配内存不同,该函数不能使用free来释放内存,也不能用*alloc类函数进行二次处理,销毁由他创建的内存只需要函数返回即可。

alloca优点:

  1. alloca函数被编译器作为内联函数处理,直接调整栈指针来实现,因此不需要维护空闲内存表,执行速度快于malloc
  2. alloca函数分配的内存不需要专门释放,随栈指针移除而释放,即函数返回
  3. 在使用longjmp等非局部跳转函数时,malloc使用不慎会导致内存泄漏(有时甚至不可避免),但alloca不会

进程的创建

fork函数

#include <unistd.h>

pid_t fork();

返回

  • 0:子进程
  • -1:错误
  • pid:父进程得到子进程pid

fork函数的工作

  1. 为子进程分配性内存和内核数据结构,复制程序文本段
  2. 复制原来的进程上下文(栈段、堆段和数据段)给新进程(意味着对于某些数据是共享的,如文件描述符(内核基于dup函数实现)
    1. 子进程继承了父进程的信号处理方式
  3. 在进程集添加子进程
  4. fork执行结束,返回给两个进程
  5. 两进程独立执行,顺序取决于调度

fork函数通常的行文

#include <unistd.h>

int main(int argc, char **argv)
{
    pid_t child_pid;

    switch (child_pid = fork())
    {
    case -1:
        perror("fork wrong:")
        break;
    case 0:
        /* child action */
        break;
    default:
        /* parent action */
        break;
    }
    return 0;
}

文件的共享

父子进程的文件描述符指向相同的文件,任意进程对文件属性(如偏移量)的修改都会影响到其他进程(举例:相同的标准输出和标准输入)

内存语义

不是简单是复制数据段(因为fork完后往往会exec系列函数调用,也就是内核进行的数据复制几乎成了浪费),而是使用小聪明对一些情况进行优化:

  • 内核将进程代码段设为只读,fork时只是创建页表项指向父进程的物理内存页帧
  • 数据段采用写时复制,只有某一进程对共享的内存数据进行写操作时,才会复制。

fork引发的竞争问题

论述了一大堆,结果就是谁先谁后依赖linux版本和/proc/sys/kernel/sched_child_run_first专有文件设置。

进程的执行

exec系列

int execv(const char*path, const char*arg0, ...);
int execl(const char*path, const char*argv[]);
int execvp(const char*path, const char*arg0, ...);
int execlp(const char*path, const char*argv[]);
  • path指明可执行程序文件的路径
  • arg指明执行程序的参数

调用成功则没有返回值,否则返回-1。

区别:

  • 以v结尾的表示参数作为vector整体传入,以l结尾的表示逐个列举的方式
  • 没有p结尾的要求指明可执行文件的绝对路径,以p结尾的可不写绝对路径,因为会优先从$PATH中查找程序

进程的监控

wait系列

pid_t wait(int * status);

阻塞的形式等待一个子进程结束,返回结束的子进程的pid并保存返回的状态,不存在子进程则返回-1

pid_t waitpid(pid_t pid, int * status, int options);

具体的形式等待一个子进程结束,返回结束的子进程的pid并保存返回的状态,不存在子进程则返回-1

  • pid = -1:等价于wait,等待任意一个子进程结束

  • pid = > 0:等待进程IDpid的子进程结束

  • pid = 0:等待进程组IDpid组中任意一个子进程结束

  • pid < -1:等待进程组IDpid绝对值中任意一个子进程结束

  • 0 阻塞父进程,同wait

  • WNOHANG

  • WUNTRACE

状态监测宏

进程的终止

exit()

#include <unistd.h>

void exit(int status);

将所有资源归还内核,status表示进程退出状态,可以由父进程wait函数获得

posted @ 2021-05-30 13:35  dwr2001  阅读(100)  评论(0编辑  收藏  举报