547 lines
13 KiB
C
547 lines
13 KiB
C
|
// SPDX-License-Identifier: GPL-2.0
|
||
|
/*
|
||
|
* Vidtv serves as a reference DVB driver and helps validate the existing APIs
|
||
|
* in the media subsystem. It can also aid developers working on userspace
|
||
|
* applications.
|
||
|
*
|
||
|
* This file contains the multiplexer logic for TS packets from different
|
||
|
* elementary streams
|
||
|
*
|
||
|
* Loosely based on libavcodec/mpegtsenc.c
|
||
|
*
|
||
|
* Copyright (C) 2020 Daniel W. S. Almeida
|
||
|
*/
|
||
|
|
||
|
#include <linux/delay.h>
|
||
|
#include <linux/dev_printk.h>
|
||
|
#include <linux/jiffies.h>
|
||
|
#include <linux/kernel.h>
|
||
|
#include <linux/math64.h>
|
||
|
#include <linux/ratelimit.h>
|
||
|
#include <linux/slab.h>
|
||
|
#include <linux/types.h>
|
||
|
#include <linux/vmalloc.h>
|
||
|
|
||
|
#include "vidtv_channel.h"
|
||
|
#include "vidtv_common.h"
|
||
|
#include "vidtv_encoder.h"
|
||
|
#include "vidtv_mux.h"
|
||
|
#include "vidtv_pes.h"
|
||
|
#include "vidtv_psi.h"
|
||
|
#include "vidtv_ts.h"
|
||
|
|
||
|
static struct vidtv_mux_pid_ctx
|
||
|
*vidtv_mux_get_pid_ctx(struct vidtv_mux *m, u16 pid)
|
||
|
{
|
||
|
struct vidtv_mux_pid_ctx *ctx;
|
||
|
|
||
|
hash_for_each_possible(m->pid_ctx, ctx, h, pid)
|
||
|
if (ctx->pid == pid)
|
||
|
return ctx;
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
static struct vidtv_mux_pid_ctx
|
||
|
*vidtv_mux_create_pid_ctx_once(struct vidtv_mux *m, u16 pid)
|
||
|
{
|
||
|
struct vidtv_mux_pid_ctx *ctx;
|
||
|
|
||
|
ctx = vidtv_mux_get_pid_ctx(m, pid);
|
||
|
if (ctx)
|
||
|
return ctx;
|
||
|
|
||
|
ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
|
||
|
if (!ctx)
|
||
|
return NULL;
|
||
|
|
||
|
ctx->pid = pid;
|
||
|
ctx->cc = 0;
|
||
|
hash_add(m->pid_ctx, &ctx->h, pid);
|
||
|
|
||
|
return ctx;
|
||
|
}
|
||
|
|
||
|
static void vidtv_mux_pid_ctx_destroy(struct vidtv_mux *m)
|
||
|
{
|
||
|
struct vidtv_mux_pid_ctx *ctx;
|
||
|
struct hlist_node *tmp;
|
||
|
int bkt;
|
||
|
|
||
|
hash_for_each_safe(m->pid_ctx, bkt, tmp, ctx, h) {
|
||
|
hash_del(&ctx->h);
|
||
|
kfree(ctx);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static int vidtv_mux_pid_ctx_init(struct vidtv_mux *m)
|
||
|
{
|
||
|
struct vidtv_psi_table_pat_program *p = m->si.pat->program;
|
||
|
u16 pid;
|
||
|
|
||
|
hash_init(m->pid_ctx);
|
||
|
/* push the pcr pid ctx */
|
||
|
if (!vidtv_mux_create_pid_ctx_once(m, m->pcr_pid))
|
||
|
return -ENOMEM;
|
||
|
/* push the NULL packet pid ctx */
|
||
|
if (!vidtv_mux_create_pid_ctx_once(m, TS_NULL_PACKET_PID))
|
||
|
goto free;
|
||
|
/* push the PAT pid ctx */
|
||
|
if (!vidtv_mux_create_pid_ctx_once(m, VIDTV_PAT_PID))
|
||
|
goto free;
|
||
|
/* push the SDT pid ctx */
|
||
|
if (!vidtv_mux_create_pid_ctx_once(m, VIDTV_SDT_PID))
|
||
|
goto free;
|
||
|
/* push the NIT pid ctx */
|
||
|
if (!vidtv_mux_create_pid_ctx_once(m, VIDTV_NIT_PID))
|
||
|
goto free;
|
||
|
/* push the EIT pid ctx */
|
||
|
if (!vidtv_mux_create_pid_ctx_once(m, VIDTV_EIT_PID))
|
||
|
goto free;
|
||
|
|
||
|
/* add a ctx for all PMT sections */
|
||
|
while (p) {
|
||
|
pid = vidtv_psi_get_pat_program_pid(p);
|
||
|
vidtv_mux_create_pid_ctx_once(m, pid);
|
||
|
p = p->next;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
|
||
|
free:
|
||
|
vidtv_mux_pid_ctx_destroy(m);
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
static void vidtv_mux_update_clk(struct vidtv_mux *m)
|
||
|
{
|
||
|
/* call this at every thread iteration */
|
||
|
u64 elapsed_time;
|
||
|
|
||
|
m->timing.past_jiffies = m->timing.current_jiffies;
|
||
|
m->timing.current_jiffies = get_jiffies_64();
|
||
|
|
||
|
elapsed_time = jiffies_to_usecs(m->timing.current_jiffies -
|
||
|
m->timing.past_jiffies);
|
||
|
|
||
|
/* update the 27Mhz clock proportionally to the elapsed time */
|
||
|
m->timing.clk += (CLOCK_UNIT_27MHZ / USEC_PER_SEC) * elapsed_time;
|
||
|
}
|
||
|
|
||
|
static u32 vidtv_mux_push_si(struct vidtv_mux *m)
|
||
|
{
|
||
|
struct vidtv_psi_pat_write_args pat_args = {
|
||
|
.buf = m->mux_buf,
|
||
|
.buf_sz = m->mux_buf_sz,
|
||
|
.pat = m->si.pat,
|
||
|
};
|
||
|
struct vidtv_psi_pmt_write_args pmt_args = {
|
||
|
.buf = m->mux_buf,
|
||
|
.buf_sz = m->mux_buf_sz,
|
||
|
.pcr_pid = m->pcr_pid,
|
||
|
};
|
||
|
struct vidtv_psi_sdt_write_args sdt_args = {
|
||
|
.buf = m->mux_buf,
|
||
|
.buf_sz = m->mux_buf_sz,
|
||
|
.sdt = m->si.sdt,
|
||
|
};
|
||
|
struct vidtv_psi_nit_write_args nit_args = {
|
||
|
.buf = m->mux_buf,
|
||
|
.buf_sz = m->mux_buf_sz,
|
||
|
.nit = m->si.nit,
|
||
|
|
||
|
};
|
||
|
struct vidtv_psi_eit_write_args eit_args = {
|
||
|
.buf = m->mux_buf,
|
||
|
.buf_sz = m->mux_buf_sz,
|
||
|
.eit = m->si.eit,
|
||
|
};
|
||
|
u32 initial_offset = m->mux_buf_offset;
|
||
|
struct vidtv_mux_pid_ctx *pat_ctx;
|
||
|
struct vidtv_mux_pid_ctx *pmt_ctx;
|
||
|
struct vidtv_mux_pid_ctx *sdt_ctx;
|
||
|
struct vidtv_mux_pid_ctx *nit_ctx;
|
||
|
struct vidtv_mux_pid_ctx *eit_ctx;
|
||
|
u32 nbytes;
|
||
|
u16 pmt_pid;
|
||
|
u32 i;
|
||
|
|
||
|
pat_ctx = vidtv_mux_get_pid_ctx(m, VIDTV_PAT_PID);
|
||
|
sdt_ctx = vidtv_mux_get_pid_ctx(m, VIDTV_SDT_PID);
|
||
|
nit_ctx = vidtv_mux_get_pid_ctx(m, VIDTV_NIT_PID);
|
||
|
eit_ctx = vidtv_mux_get_pid_ctx(m, VIDTV_EIT_PID);
|
||
|
|
||
|
pat_args.offset = m->mux_buf_offset;
|
||
|
pat_args.continuity_counter = &pat_ctx->cc;
|
||
|
|
||
|
m->mux_buf_offset += vidtv_psi_pat_write_into(&pat_args);
|
||
|
|
||
|
for (i = 0; i < m->si.pat->num_pmt; ++i) {
|
||
|
pmt_pid = vidtv_psi_pmt_get_pid(m->si.pmt_secs[i],
|
||
|
m->si.pat);
|
||
|
|
||
|
if (pmt_pid > TS_LAST_VALID_PID) {
|
||
|
dev_warn_ratelimited(m->dev,
|
||
|
"PID: %d not found\n", pmt_pid);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
pmt_ctx = vidtv_mux_get_pid_ctx(m, pmt_pid);
|
||
|
|
||
|
pmt_args.offset = m->mux_buf_offset;
|
||
|
pmt_args.pmt = m->si.pmt_secs[i];
|
||
|
pmt_args.pid = pmt_pid;
|
||
|
pmt_args.continuity_counter = &pmt_ctx->cc;
|
||
|
|
||
|
/* write each section into buffer */
|
||
|
m->mux_buf_offset += vidtv_psi_pmt_write_into(&pmt_args);
|
||
|
}
|
||
|
|
||
|
sdt_args.offset = m->mux_buf_offset;
|
||
|
sdt_args.continuity_counter = &sdt_ctx->cc;
|
||
|
|
||
|
m->mux_buf_offset += vidtv_psi_sdt_write_into(&sdt_args);
|
||
|
|
||
|
nit_args.offset = m->mux_buf_offset;
|
||
|
nit_args.continuity_counter = &nit_ctx->cc;
|
||
|
|
||
|
m->mux_buf_offset += vidtv_psi_nit_write_into(&nit_args);
|
||
|
|
||
|
eit_args.offset = m->mux_buf_offset;
|
||
|
eit_args.continuity_counter = &eit_ctx->cc;
|
||
|
|
||
|
m->mux_buf_offset += vidtv_psi_eit_write_into(&eit_args);
|
||
|
|
||
|
nbytes = m->mux_buf_offset - initial_offset;
|
||
|
|
||
|
m->num_streamed_si++;
|
||
|
|
||
|
return nbytes;
|
||
|
}
|
||
|
|
||
|
static u32 vidtv_mux_push_pcr(struct vidtv_mux *m)
|
||
|
{
|
||
|
struct pcr_write_args args = {};
|
||
|
struct vidtv_mux_pid_ctx *ctx;
|
||
|
u32 nbytes = 0;
|
||
|
|
||
|
ctx = vidtv_mux_get_pid_ctx(m, m->pcr_pid);
|
||
|
args.dest_buf = m->mux_buf;
|
||
|
args.pid = m->pcr_pid;
|
||
|
args.buf_sz = m->mux_buf_sz;
|
||
|
args.continuity_counter = &ctx->cc;
|
||
|
|
||
|
/* the 27Mhz clock will feed both parts of the PCR bitfield */
|
||
|
args.pcr = m->timing.clk;
|
||
|
|
||
|
nbytes += vidtv_ts_pcr_write_into(args);
|
||
|
m->mux_buf_offset += nbytes;
|
||
|
|
||
|
m->num_streamed_pcr++;
|
||
|
|
||
|
return nbytes;
|
||
|
}
|
||
|
|
||
|
static bool vidtv_mux_should_push_pcr(struct vidtv_mux *m)
|
||
|
{
|
||
|
u64 next_pcr_at;
|
||
|
|
||
|
if (m->num_streamed_pcr == 0)
|
||
|
return true;
|
||
|
|
||
|
next_pcr_at = m->timing.start_jiffies +
|
||
|
usecs_to_jiffies(m->num_streamed_pcr *
|
||
|
m->timing.pcr_period_usecs);
|
||
|
|
||
|
return time_after64(m->timing.current_jiffies, next_pcr_at);
|
||
|
}
|
||
|
|
||
|
static bool vidtv_mux_should_push_si(struct vidtv_mux *m)
|
||
|
{
|
||
|
u64 next_si_at;
|
||
|
|
||
|
if (m->num_streamed_si == 0)
|
||
|
return true;
|
||
|
|
||
|
next_si_at = m->timing.start_jiffies +
|
||
|
usecs_to_jiffies(m->num_streamed_si *
|
||
|
m->timing.si_period_usecs);
|
||
|
|
||
|
return time_after64(m->timing.current_jiffies, next_si_at);
|
||
|
}
|
||
|
|
||
|
static u32 vidtv_mux_packetize_access_units(struct vidtv_mux *m,
|
||
|
struct vidtv_encoder *e)
|
||
|
{
|
||
|
struct pes_write_args args = {
|
||
|
.dest_buf = m->mux_buf,
|
||
|
.dest_buf_sz = m->mux_buf_sz,
|
||
|
.pid = be16_to_cpu(e->es_pid),
|
||
|
.encoder_id = e->id,
|
||
|
.stream_id = be16_to_cpu(e->stream_id),
|
||
|
.send_pts = true, /* forbidden value '01'... */
|
||
|
.send_dts = false, /* ...for PTS_DTS flags */
|
||
|
};
|
||
|
struct vidtv_access_unit *au = e->access_units;
|
||
|
u32 initial_offset = m->mux_buf_offset;
|
||
|
struct vidtv_mux_pid_ctx *pid_ctx;
|
||
|
u32 nbytes = 0;
|
||
|
u8 *buf = NULL;
|
||
|
|
||
|
/* see SMPTE 302M clause 6.4 */
|
||
|
if (args.encoder_id == S302M) {
|
||
|
args.send_dts = false;
|
||
|
args.send_pts = true;
|
||
|
}
|
||
|
|
||
|
pid_ctx = vidtv_mux_create_pid_ctx_once(m, be16_to_cpu(e->es_pid));
|
||
|
args.continuity_counter = &pid_ctx->cc;
|
||
|
|
||
|
while (au) {
|
||
|
buf = e->encoder_buf + au->offset;
|
||
|
args.from = buf;
|
||
|
args.access_unit_len = au->nbytes;
|
||
|
args.dest_offset = m->mux_buf_offset;
|
||
|
args.pts = au->pts;
|
||
|
args.pcr = m->timing.clk;
|
||
|
|
||
|
m->mux_buf_offset += vidtv_pes_write_into(&args);
|
||
|
|
||
|
au = au->next;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* clear the encoder state once the ES data has been written to the mux
|
||
|
* buffer
|
||
|
*/
|
||
|
e->clear(e);
|
||
|
|
||
|
nbytes = m->mux_buf_offset - initial_offset;
|
||
|
return nbytes;
|
||
|
}
|
||
|
|
||
|
static u32 vidtv_mux_poll_encoders(struct vidtv_mux *m)
|
||
|
{
|
||
|
struct vidtv_channel *cur_chnl = m->channels;
|
||
|
struct vidtv_encoder *e = NULL;
|
||
|
u32 nbytes = 0;
|
||
|
u32 au_nbytes;
|
||
|
|
||
|
while (cur_chnl) {
|
||
|
e = cur_chnl->encoders;
|
||
|
|
||
|
while (e) {
|
||
|
e->encode(e);
|
||
|
/* get the TS packets into the mux buffer */
|
||
|
au_nbytes = vidtv_mux_packetize_access_units(m, e);
|
||
|
nbytes += au_nbytes;
|
||
|
m->mux_buf_offset += au_nbytes;
|
||
|
/* grab next encoder */
|
||
|
e = e->next;
|
||
|
}
|
||
|
|
||
|
/* grab the next channel */
|
||
|
cur_chnl = cur_chnl->next;
|
||
|
}
|
||
|
|
||
|
return nbytes;
|
||
|
}
|
||
|
|
||
|
static u32 vidtv_mux_pad_with_nulls(struct vidtv_mux *m, u32 npkts)
|
||
|
{
|
||
|
struct null_packet_write_args args = {
|
||
|
.dest_buf = m->mux_buf,
|
||
|
.buf_sz = m->mux_buf_sz,
|
||
|
.dest_offset = m->mux_buf_offset,
|
||
|
};
|
||
|
u32 initial_offset = m->mux_buf_offset;
|
||
|
struct vidtv_mux_pid_ctx *ctx;
|
||
|
u32 nbytes;
|
||
|
u32 i;
|
||
|
|
||
|
ctx = vidtv_mux_get_pid_ctx(m, TS_NULL_PACKET_PID);
|
||
|
|
||
|
args.continuity_counter = &ctx->cc;
|
||
|
|
||
|
for (i = 0; i < npkts; ++i) {
|
||
|
m->mux_buf_offset += vidtv_ts_null_write_into(args);
|
||
|
args.dest_offset = m->mux_buf_offset;
|
||
|
}
|
||
|
|
||
|
nbytes = m->mux_buf_offset - initial_offset;
|
||
|
|
||
|
/* sanity check */
|
||
|
if (nbytes != npkts * TS_PACKET_LEN)
|
||
|
dev_err_ratelimited(m->dev, "%d != %d\n",
|
||
|
nbytes, npkts * TS_PACKET_LEN);
|
||
|
|
||
|
return nbytes;
|
||
|
}
|
||
|
|
||
|
static void vidtv_mux_clear(struct vidtv_mux *m)
|
||
|
{
|
||
|
/* clear the packets currently in the mux */
|
||
|
memset(m->mux_buf, 0, m->mux_buf_sz * sizeof(*m->mux_buf));
|
||
|
/* point to the beginning of the buffer again */
|
||
|
m->mux_buf_offset = 0;
|
||
|
}
|
||
|
|
||
|
#define ERR_RATE 10000000
|
||
|
static void vidtv_mux_tick(struct work_struct *work)
|
||
|
{
|
||
|
struct vidtv_mux *m = container_of(work,
|
||
|
struct vidtv_mux,
|
||
|
mpeg_thread);
|
||
|
struct dtv_frontend_properties *c = &m->fe->dtv_property_cache;
|
||
|
u32 tot_bits = 0;
|
||
|
u32 nbytes;
|
||
|
u32 npkts;
|
||
|
|
||
|
while (m->streaming) {
|
||
|
nbytes = 0;
|
||
|
|
||
|
vidtv_mux_update_clk(m);
|
||
|
|
||
|
if (vidtv_mux_should_push_pcr(m))
|
||
|
nbytes += vidtv_mux_push_pcr(m);
|
||
|
|
||
|
if (vidtv_mux_should_push_si(m))
|
||
|
nbytes += vidtv_mux_push_si(m);
|
||
|
|
||
|
nbytes += vidtv_mux_poll_encoders(m);
|
||
|
nbytes += vidtv_mux_pad_with_nulls(m, 256);
|
||
|
|
||
|
npkts = nbytes / TS_PACKET_LEN;
|
||
|
|
||
|
/* if the buffer is not aligned there is a bug somewhere */
|
||
|
if (nbytes % TS_PACKET_LEN)
|
||
|
dev_err_ratelimited(m->dev, "Misaligned buffer\n");
|
||
|
|
||
|
if (m->on_new_packets_available_cb)
|
||
|
m->on_new_packets_available_cb(m->priv,
|
||
|
m->mux_buf,
|
||
|
npkts);
|
||
|
|
||
|
vidtv_mux_clear(m);
|
||
|
|
||
|
/*
|
||
|
* Update bytes and packet counts at DVBv5 stats
|
||
|
*
|
||
|
* For now, both pre and post bit counts are identical,
|
||
|
* but post BER count can be lower than pre BER, if the error
|
||
|
* correction logic discards packages.
|
||
|
*/
|
||
|
c->pre_bit_count.stat[0].uvalue = nbytes * 8;
|
||
|
c->post_bit_count.stat[0].uvalue = nbytes * 8;
|
||
|
c->block_count.stat[0].uvalue += npkts;
|
||
|
|
||
|
/*
|
||
|
* Even without any visible errors for the user, the pre-BER
|
||
|
* stats usually have an error range up to 1E-6. So,
|
||
|
* add some random error increment count to it.
|
||
|
*
|
||
|
* Please notice that this is a poor guy's implementation,
|
||
|
* as it will produce one corrected bit error every time
|
||
|
* ceil(total bytes / ERR_RATE) is incremented, without
|
||
|
* any sort of (pseudo-)randomness.
|
||
|
*/
|
||
|
tot_bits += nbytes * 8;
|
||
|
if (tot_bits > ERR_RATE) {
|
||
|
c->pre_bit_error.stat[0].uvalue++;
|
||
|
tot_bits -= ERR_RATE;
|
||
|
}
|
||
|
|
||
|
usleep_range(VIDTV_SLEEP_USECS, VIDTV_MAX_SLEEP_USECS);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void vidtv_mux_start_thread(struct vidtv_mux *m)
|
||
|
{
|
||
|
if (m->streaming) {
|
||
|
dev_warn_ratelimited(m->dev, "Already streaming. Skipping.\n");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
m->streaming = true;
|
||
|
m->timing.start_jiffies = get_jiffies_64();
|
||
|
schedule_work(&m->mpeg_thread);
|
||
|
}
|
||
|
|
||
|
void vidtv_mux_stop_thread(struct vidtv_mux *m)
|
||
|
{
|
||
|
if (m->streaming) {
|
||
|
m->streaming = false; /* thread will quit */
|
||
|
cancel_work_sync(&m->mpeg_thread);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct vidtv_mux *vidtv_mux_init(struct dvb_frontend *fe,
|
||
|
struct device *dev,
|
||
|
struct vidtv_mux_init_args *args)
|
||
|
{
|
||
|
struct vidtv_mux *m;
|
||
|
|
||
|
m = kzalloc(sizeof(*m), GFP_KERNEL);
|
||
|
if (!m)
|
||
|
return NULL;
|
||
|
|
||
|
m->dev = dev;
|
||
|
m->fe = fe;
|
||
|
m->timing.pcr_period_usecs = args->pcr_period_usecs;
|
||
|
m->timing.si_period_usecs = args->si_period_usecs;
|
||
|
|
||
|
m->mux_rate_kbytes_sec = args->mux_rate_kbytes_sec;
|
||
|
|
||
|
m->on_new_packets_available_cb = args->on_new_packets_available_cb;
|
||
|
|
||
|
m->mux_buf = vzalloc(args->mux_buf_sz);
|
||
|
if (!m->mux_buf)
|
||
|
goto free_mux;
|
||
|
|
||
|
m->mux_buf_sz = args->mux_buf_sz;
|
||
|
|
||
|
m->pcr_pid = args->pcr_pid;
|
||
|
m->transport_stream_id = args->transport_stream_id;
|
||
|
m->priv = args->priv;
|
||
|
m->network_id = args->network_id;
|
||
|
m->network_name = kstrdup(args->network_name, GFP_KERNEL);
|
||
|
m->timing.current_jiffies = get_jiffies_64();
|
||
|
|
||
|
if (args->channels)
|
||
|
m->channels = args->channels;
|
||
|
else
|
||
|
if (vidtv_channels_init(m) < 0)
|
||
|
goto free_mux_buf;
|
||
|
|
||
|
/* will alloc data for pmt_sections after initializing pat */
|
||
|
if (vidtv_channel_si_init(m) < 0)
|
||
|
goto free_channels;
|
||
|
|
||
|
INIT_WORK(&m->mpeg_thread, vidtv_mux_tick);
|
||
|
|
||
|
if (vidtv_mux_pid_ctx_init(m) < 0)
|
||
|
goto free_channel_si;
|
||
|
|
||
|
return m;
|
||
|
|
||
|
free_channel_si:
|
||
|
vidtv_channel_si_destroy(m);
|
||
|
free_channels:
|
||
|
vidtv_channels_destroy(m);
|
||
|
free_mux_buf:
|
||
|
vfree(m->mux_buf);
|
||
|
free_mux:
|
||
|
kfree(m);
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
void vidtv_mux_destroy(struct vidtv_mux *m)
|
||
|
{
|
||
|
vidtv_mux_stop_thread(m);
|
||
|
vidtv_mux_pid_ctx_destroy(m);
|
||
|
vidtv_channel_si_destroy(m);
|
||
|
vidtv_channels_destroy(m);
|
||
|
kfree(m->network_name);
|
||
|
vfree(m->mux_buf);
|
||
|
kfree(m);
|
||
|
}
|