Linux C开发常见问题

写在前面

本文用作日常工作中遇到的问题(偏概念性)总结,捎带手记录面试题 ,不定时更新。以便自己日后查询,也供大家参考,如有错误之处,还请指正。

本文一些内容为原创,一些内容为从网上摘抄,如有侵权,请私信,必删!

C

进程与线程

什么是进程?什么是线程?

  1. 进程
    进程是程序的一次执行过程,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有自己的内存空间。
    进程有五种状态:初始态,执行态,阻塞态,就绪状态,终止状态。
  2. 线程
    线程是CPU调度和分派的基本单位,它可与同属一个进程的其他线程共享进程所拥有的全部资源。

进程与线程之间有什么联系和区别?

  1. 联系
    线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,只有一个线程的进程称为单线程。
  2. 区别
  • 根本区别:进程是操作系统进行资源分配的最小单位,而线程是任务调度和执行的基本单位。

  • 开销方面:每个进程有自己独立的代码和数据空间,进程之间切换会有较大的开销;线程可以看做一个轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换开销较小。

  • 所处环境:在操作系统中可以运行多个进程;在一个进程中,可以运行多个线程。

  • 内存分配:操作系统会为每个进程分配独立的内存空间;属于同一进程的线程共享该进程的内存空间。

  • 包含关系:没有线程的进程可以看做是单线程的,如果一个进程有多个线程,则执行过程不是一条线,而是多条线并行进行,则称这个进程是多线程的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

进程与进程,线程与线程,进程与线程之间如何通信?

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

IPC的方式通常有管道(包括无名管道和命名管道)消息队列信号量共享存储SocketStream。其中SocketStream支持不同主机上的两个进程通信。

管道
无名管道

管道通常指无名管道,是UNIX系统IPC最古老的形式。

  1. 特点
  1. 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
  2. 它只能用于具有亲缘关系的进程之间
  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的readwrite等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中
FIFO

FIFO,也称为命名管道,它是一种文件类型。

  1. 特点
  1. FIFO可以在无关的进程之间交换数据,与无名管道不同。
  2. FIFO有路径名与之相关联,它以一种特殊文件形式存在于文件系统中。
总结
  1. 管道:速度慢,容量有限,只有父子进程间可以通信。
  2. FIFO:任何进程间都能通信,但速度慢。
  3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上次没有读完数据的问题。
  4. 信号量:不能传递复杂消息,只能用来同步与互斥。
  5. 共享内存区:能够很容易控制容量,速度快,但是要保持同步,比如一个进程在写的是时候,另一个进程需要考虑读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用于线程间的通信,不过没有这个必要,线程间本来就已经共享同一进程的一块内存。

线程与线程之间通信

互斥锁

对于互斥的访问一个全局变量

锁机制是同一时刻只允许一个线程执行关键部分的代码。

条件变量

条件变量是利用线程间共享全局变量进行同步的一种机制。条件变量上的基本操作有:触发条件(当条件变为true时);等待条件,挂起线程,直到其他线程触发条件。

信号量

是一个整数计数器,其数值表示空闲临界资源的数量。

读写锁

一个简单的线程与进程的例子

由于pthread.h不是Linux默认库,则需要在编译时加-lpthread,如下:

  • gcc thread.c -o thread -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

void *print_a(void *);
void *print_b(void *);

int main()
{
    pthread_t t0;
    pthread_t t1;

    if (pthread_create(&t0, NULL, print_a, NULL) == -1) {
        puts("failed to create pthread t0!");
        exit(1);
    }

    if (pthread_create(&t1, NULL, print_b, NULL) == -1) {
        puts("failed to create pthread t1");
        exit(1);
    }

    void *result;
    if (pthread_join(t0, &result) == -1) {
        puts("failed to recollect t0");
        exit(1);
    }

    if (pthread_join(t1, &result) == -1) {
        puts("failed to recollect t1");
        exit(1);
    }

    return 0;
}

void *print_a(void *a) {
    int i = 0;
        for (i; i < 10; i++) {
        sleep(1);
        puts("aa");
    }
    return NULL;
}

void *print_b(void *b) {
    int i = 0;
    for (i; i < 20; i++) {
        sleep(1);
        puts("bb");
    }
    return NULL;
}

僵尸进程和孤儿进程

该小结从进程为何会fork()两次转载

孤儿进程

孤儿进程是指父进程在子进程结束之前死亡(exit或return),如下图所示:

孤儿进程

但是孤儿进程并不会像上面画的那样持续很长时间,当系统发现孤儿进程时,init进程就会收养孤儿进程,成为它的父亲,chlid进程exit后的资源回收就由init进程实现。

僵尸进程

僵尸进程是指子进程在父进程之前结束了,但是父进程没有用waitwaitpid回收子进程。如下图所示:

僵尸进程

父进程没有用wait回收子进程并不说明它不会回收子进程。子进程在结束的时候会给其父进程发送一个SIGCHILD信号,父进程默认是忽略SIGCHILD信号的,如果父进程通过signal()函数设置了SIGCHILD的信号处理函数,则在信号处理函数中可以回收子进程的资源。

事实上,即便是父进程没有设置SIGCHILD的信号处理函数,也没有关系,因为在父进程结束之前,子进程可以一直保持僵尸状态,当父进程结束后,init进程就会负责回收子进程的资源。

但是,如果父进程是一个服务器进程,一直循环着不退出,那子进程就会一直保持僵尸状态。虽然僵尸状态进程不会占用任何内存资源,但是过多的僵尸进程还是会影响系统性能的。

两次fork()的用法

两次fork()的流程如下所示:

两次fork()

如上图所示,为了避免子进程child成为僵尸进程,我们可以人为地创建一个子进程child1,再让child1成为工作子进程child2的父进程,child2出生后child1退出,这个时候child2相当于是child1产生的孤儿进程,这个孤儿进程由系统进程init回收。这样,当child2退出的时候,init进程就会回收child2的资源,child2就不会变成僵尸进程了。

<unix环境高级编程>这本书里提供了两次fork的一个例子,代码如下:

int main(void)
{
	pid_t        pid;
 
	if ( (pid = fork()) < 0)
          err_sys("fork error");
	else if (pid == 0) 
		{                /* first child */
           if ( (pid = fork()) < 0)
                        err_sys("fork error");
           else if (pid > 0)
                 exit(0);        /* parent from second fork == first child */
 
                /* We're the second child; our parent becomes init as soon
                   as our real parent calls exit() in the statement above.
                   Here's where we'd continue executing, knowing that when
                   we're done, init will reap our status. */
 
            sleep(2);
            printf("second child, parent pid = %d\n", getppid());
            exit(0);
        }
 
    if (waitpid(pid, NULL, 0) != pid)        /* wait for first child */
            err_sys("waitpid error");
 
        /* We're the parent (the original process); we continue executing,
           knowing that we're not the parent of the second child. */
 
    exit(0);
}
       理所当然,第二个子进程的父进程是进程号为1的init进程。

我的理解是,两次fork()是为了避免程序出现僵尸进程,通过中间进程child1来充当父进程与工作子进程child2之间的桥梁,通过结束child1,使得child2变为孤儿进程,结束后由init进程回收相关资源。

C语言基础

数据类型与字节之间转换关系

1字节 = 8个二进制(bit) 0000 0000 - 1111 1111 = 0-255(十进制) = 00-ff(十六机制)

数据类型字节数
byte1
short2
int4
long8
float4
double8
char2

const关键字的用法

若一个变量前用const修饰,则该变量的值无法改变,相当于“只读”的状态。

当一个变量用const修饰时,需要定义时就进行初始化赋值

  • 用于修饰局部变量,以下两种赋值方式为等价的
const int i = 10;
int const i = 10;
  • 用于修饰常量指针与指针常量

常量指针指的是指针指向的内容是常量,常量指针不能通过这个指针改变变量的值,但是可以通过其他引用来改变变量的值,例如:

const int *i;
int const *i;
/* 以上两种声明方式是等价的 */
int a = 8;
int b = 9;
const int *n = &a;
n = &b;

指针常量是说指针本身是一个常量,不能再指向其他的地址,即指向的地址不能改变,但是地址中保存的数值是可以改变的,如:

int * const i;

int k = 8;
int *p = &k;
int * const n = &a;
*p = 8;

我刚开始看这个也有点乱,什么指针常量,常量指针的,绕过来绕过去的。而且如何修改这些值呢?但是仔细想一下,就可以很好的区分开两者的区别了。

  • 常量指针,不能通过指针修改所指常量的值(类似于*p = 20的操作),但是可以通过修改指针的指向(原来指向a,现在指向b)或直接修改指向常量的值(假如指针指向a,初始值为10,那么可以通过重新给a赋值),来修改常量的值,举个栗子:
int n = 10;
const int *a = &n;		//声明及初始化,此时*a=10
*a = 20;		//错误,不可以通过指针改变常量的值,若要改变,可以从下面的两种方法里任选其一
/*方法一*/
int b = 20;
a = &b;		//正确,通过修改指针的指向,此时*a = 20,注意,这个时候指针a的值已经改变了
/*方法二*/
n = 20;		//正确,绕过指针,直接修改常量的值
  • 指针常量,指针的值打死也不能修改,也就是说,这个指针这辈子就叫这个名字(指针的值不能改变),但是可以通过修改指针指向的值来进行操作,举个栗子:
int a = 10;
int * const n = &a;

*n = 20;		//正确,常量指针不允许的操作,在这里可以,修改指针指向的值,指针本身没有改变

int b = 20;
n = &b;		//错误,不能修改指针的值
  • 总结一哈,在修改指针常量或常量指针的时候,*const谁在前面,那么谁就不能被改变,因为内存区此时为其分配的是一个只读的内存空间。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值