linux编程杂记

目录

前记

一、程序的编译

(1)编译的过程

(2)程序运行原理

(3)程序空间

(4)进程内部的资源

(5)进程的状态

二、进程

三、线程

四、进程与线程的区别

五、进程间通信:

六、线程间通信:

注:参考资料


前记

linux系统编程中一些杂记。重新整理下。关于这一块,当时是有看过一本《程序的自我修养》,对编译原理以及堆栈等讲的还挺详细了。这会也忘得差不多了,就目前根据是实际使用过程中,把有用的都简单整理下,方便以后直接能记起来。

一、程序的编译

进程就是执行中的程序,程序要执行前需加载到内存中. 一个程序可以执行多次,也就可以多个进程。

(1)编译的过程

编译过程: 预处理 ----> 编译 ----> 汇编 ----> 链接

1)预处理(Pre-processing)

在该阶段,编译器将C源代码中的包含的头文件如stdio.h添加进来

参数:”-E”

用法:gcc -E hello.c -o hello.i

作用:将hello.c预处理输出hello.i文件。

2)编译(Compiling)

第二步进行的是编译阶段,在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。

参数:”-S”

用法:gcc –S hello.i –o hello.s

作用:将预处理输出文件hello.i汇编成hello.s文件。

3)汇编(Assembling)

汇编阶段是把编译阶段生成的”.s”文件转成二进制目标代码“.o”文件

参数:“-c”

用法:gcc –c hello.s –o hello.o

作用:将汇编输出文件hello.s编译输出hello.o文件。

4)链接(Link)

在成功编译之后,就进入了链接阶段。

用法:gcc hello.o –o hello

作用:将编译输出文件hello.o链接成最终可执行文件hello。

一般我们写Makefile 就是安装上面的步骤编译.o 再链接成二进制文件,最后还要strip去掉符号,减小bin文件大小

(2)程序运行原理

一个程序镜像里分有: xx段(section)
    分有很多个段, 通常我们要注意的有: 代码段(.text),  数据段(.data), 只读数据段(.rodata)
                               未初始化的数据段(.bss)

反汇编: objdump -D  可执行文件 

: 其实就是表示一个文件内的区域(从文件的第几个字节 ---到文件内的第几个字节结束)
代码段:用于存放需要执行的代码(编译完后是机器码,反汇编时,编译器会把机器码转为相应的汇编语句)

数据段: 存放初始化过全局变量, 初始化过的静态变量。 也就是这些变量在编译时编译器分配在这个区域里的。

未初始化数据段: 存放没初始化的全局变量, 没初始化的静态变量。 这些变量在编译时,不会分配具体的空间,只是在bss段里记录所需的空间。 在程序执行时,系统才会分配具体的空间。

只读数据段: 存放字符串常量, 只读全局变量(const).  这些变量在编译时,编译器会在这个区域里分配具体的空间
    char *p = "hello world";  //字符串"hello world"的内容存入只读数据段
                          //至于指针p分配在哪个段与它的作用域有关

(3)程序空间

局部变量的空间在代码执行时才会在栈里分配出来

栈(stack): 系统里用于分配局部变量的空间区域, 分配出来的空间地址是从高往低分配的.
       系统里只有一个栈. 一个函数执行时,分从栈里分配局部变量的空间, 函数执行结束时会回收栈里分配的空间。回收的空间会重用的。
       在系统里每个进程在栈里所使用的大小空间有限制(cenos7上不超过8M).
       避免爆栈, 尽量使用动态分配空间

堆(heap) : 系统里用于动态分配空间的区域,分配出来的空间地址是从低往高分配的。
       系统里只有一个堆。 每个进程在堆里使用的空间是没有限制大小, 只要系统还有可用内存即可申请成功。
       动态分配出来的空间需要调用释放函数回收。
       malloc()   ---  free()

(4)进程内部的资源

文件描述符是进程内部的资源。不同的进程中有相同号的文件描述符,号相同并不代表所操作的对象也相同.

一个进程里默认只能有1024个文件描述符. (0-2) (3~1023) 。0,1,2是标准输出,标准输入和标准错误
    如文件描述符close后,可重用。

strace 可执行文件  //可查看此执行文件的系统调用过程
valgrind 可执行文件 //查看有没有内存泄露
nm      可执行文件  //可查看一个程序里的全局变量,函数编译时所分配的地址
file    可执行文件  //可查看程序的编译版本,架构等
readelf  程序      //可查看程序所需的C库版本等文件头信息         
ldd      程序      //可查看程序所需的动态库

strip    程序      //把程序中的符号信息去掉,无法用nm等命令查看程序.

(5)进程的状态

///进程的状态(通过man ps)
       D    Uninterruptible sleep (usually IO) 不间断睡眠(通常为IO)
       R    Running or runnable (on run queue) 正在运行或可运行(运行队列上)
       S    Interruptible sleep (waiting for an event to 可中断睡眠(等待事件完成)
            complete)
       T    Stopped, either by a job control signal or because 因为作业控制信号或正在追踪。
            it is being traced.
       X    dead (should never be seen) 死亡(永远不会被看到)
       Z    Defunct ("zombie") process, terminated but not
            reaped by its parent.   失效(“僵尸”)进程,已终止但未终止被它的父母收获。

       <    high-priority (not nice to other users)  高优先级(对其他用户不好)
       N    low-priority (nice to other users)   低优先级(对其他用户友好)
       L    has pages locked into memory (for real-time and
            custom IO)  将页面锁定在内存中(用于实时和自定义IO)
       s    is a session leader
       l    is multi-threaded (using CLONE_THREAD, like NPTL
            pthreads do)  是多线程的(使用克隆线程,比如NPTL)
       +    is in the foreground process group  在前台进程组中

二、进程

每个进程都会拥有一个唯一的id号, 也就是PID
父子进程:
    假设A进程调用了B程序, 产生了B进程,
那么对于B进程来说A进程就是它的父进程, 对A进程来说B进程就是它的子进程

1. getpid(), 得到当前进程的pid号
   getppid(), 得到当前进程的父进程pid号

2. nice(val), 设置当前进程的优先级别(-20 -- 19), 默认级别为0, -20为最高, 19最低
   setprioity(which, who, val); 
    当which为:PRIO_PROCESS, who为要设优先级别的进程号, val为优先级别数
    当为: PRIO_PGRP, who为要设的进程组的id号
    当为: PRIO_USER, who为要设的user id

3. fork() 创建一个子进程
    等于把父进程完整的复制一份副本,而这个副本就成为了子进程, 包括父进程里的变量,函数,文件描述符等,在子进程里同样具有
    子进程创建出来后,它从fork()函数后开始运行的,而不是从main函数,在子进程里得到fork()的返回值是0, 父进程得到子进程的pid号 

4. 僵尸进程: 当子进程已退出, 父进程还在运行时,则子进程变成了僵尸进程,直到被父进程收尸或父进程退出
    pid_t wait(int *status); 用于收尸
    调用此函数时会堵塞当前进程的运行,直到有子进程结束为止,函数返回值为结束的子进程的pid号
     WIFEXITED(status)用于检查是否正常退出 exit, _exit,return等主动退出的都是正常退出,如被kill则是非正常退出
    WEXITSTATUS(status)得到子进程的返回值
    
    如要等待指定的子进程退出时可用waitpid函数


5.exit // return // _exit都是正常结束进程
    exit//return结束时会关闭打开的文件描述符,处理缓冲区的数据
    _exit不会处理缓冲区的数据

6. int atexit(void (*func)(void));
    注册一个函数在进程正常退出时自动运行,可用于处理退出前的工作


7. 孤儿进程: 当父进程退出后,而子进程还在运行,则成为孤儿进程,
孤儿进程会被init(systemd)进程(pid=1)接管,所以孤儿进程的ppid=1,当孤儿进程结束运行时,init进程会收它的尸


8. vfork() 同fork()函数功能一样, 但在复制父进程时,不会完整的复制. 适用于创建一个子进程后,马上使用exec*函数调用其它程序.
    vfork会堵塞会父进程的执行, 直到子进程执行完毕.


9. demon守护进程, 也就是在后台运行的进程
    创建步骤: 1.先创建出一个孤儿进程
         2. setsid(), 让此进程成为进程组的组长并关闭终端
         3.关闭标准输出,标准错误输出
         4. 把当前的工作目录改变到"/"

10. exec调用程序
       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 execve(const char *filename, char *const argv[], char *const envp[]);
    以上函数execve才是真正的系统调用函数
    注意最后一个参数必须是NULL
    
    "l"字母表示参数是单个的参数
    "v"字母表示参数是数组形式的
    有"p"字母的表示要调用的程序在PATH环境变量里查找,
没有"p"字母的要给绝对路径
    有"e"字母的表示可指定什么环境变量给要调用的程序,没有"e"字母的表示让要调要的程序继承当前进程的环境变量


11. 环境变量的值
    在shell上env可查看当前系统的环境变量
    子进程默认会继承父进程里的环境变量,在子进程里改变环境变量不会影响父进程

     extern char **environ; 获取系统的环境变量
     getenv(char *name) 获取指定名字的环境变量值, 找不到返回NULL
     setenv(char *name, char *val, int overwrite) 设置环境变量的值
    如果指定的变量名已存在而且overwrite不为0,则改定变量的值           如果指定的变量名已存在而且overwrite为0,则不改定变量的值    

三、线程

一个进程里可以有多个线程,也就是可以多个同时执行的代码线路分支。
系统的进程调度的最小单元为线程, 进程作为资源最小分配单元

每个进程都会有一个主线程(从main函数开始执行的线程), 并且可以有多个非主线程,当主线程退出时,则整个进程和所有属于此进程的线程都会结束运行。一个线程只能属于一个进程。

一个进程里的所有线程可以共用进程的数据段,代码段,只读数据段。。。。
一个进程里的线程不用线程间的通信。    

线程库: gcc -lpthread  编译的时候需要指定带此库。

    int pthread_join(pthread_t thread, void **value_ptr);
    //调用此函数的当前线程会堵塞直到指定的ID为thread的线程结束运行为止, 并把返回值保存到value_ptr里. 并清理thread线程所使用的资源
    int pthread_detach(pthread_t thread); //分离指定的线程,当thread线程结束运行时,会自动清理所使用的资源
    int pthread_cancel(pthread_t thread); //取消线程的执行.

 对于所创建出来的线程,pthread_join或者pthread_detach必须用一个。主线程要么事分离线程,要么是等待子线程结束

四、进程与线程的区别

1.多进程和多线程的区别,前者开销大,后者开销较小。确实,这就是最基本的区别。

2.线程函数的可重入性:

要确保函数线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问时,如果要保证线程安全,则必须通过加锁的方式

可重入的判断条件:

要确保函数可重入,需满足一下几个条件:

1、不在函数内部使用静态或全局数据
2、不返回静态或全局数据,所有数据都由函数的调用者提供。
3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
4、不调用不可重入函数。

如果我们的线程函数不是线程安全的,那在多线程调用的情况下,可能导致的后果是显而易见的——共享变量的值由于不同线程的访问,可能发生不可预料的变化,进而导致程序的错误,甚至崩溃。

多数的多线程都是在同一个进程下的,它们共享该进程的全局变量,我们可以通过全局变量来实现线程间通信。如果是不同的进程下的2个线程间通信,直接参考进程间通信

多进程要并发协调工作,进程间的同步,通信是在所难免的。比如:在系统做配置更改时,需要能够通知到到对应的进程 重新加载新的参数配置运行。 就可以使用一个专门管理进程的控制程序,类似于openwrt中的proced等机制。

五、进程间通信:

在linux下有多种进程间通信的方法:无名管道、命名管道、内存映射、消

息队列、共享内存、信号量、信号、文件,套接字及环境变量等等。

无名管道(pipe)
命名管道(fifo)
内存映射(mapped memeory),
消息队列(message queue)
共享内存(shared memory)
信号量(semaphore)
信号(signal)
文件(file)
套接字(Socket):这个在自己工作时工作使用的比较多。

环境变量

一些demo代码就放到了gitgee上面

test_soure_code: 记录一些平时的demo代码

六、线程间通信:

主线程做自己的事情,生成2个子线程,task1为分离,任其自生自灭,而task2还是继续送外卖,需要等待返回。(因该还记得前面说过僵尸进程吧,线程也是需要等待的。如果不想等待,就设置线程为分离线程)

注:参考资料

参考到以下博客或资料连接:

多线程和多进程的区别(小结)_猫已经找不回了的博客-CSDN博客_多线程和多进程

多进程和多线程的区别是什么?多进程和多线程的优缺点分析_雪的季节的博客-CSDN博客_多进程和多线程的区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值